《 Spring in action》复习笔记 - 第十六章 创建 REST API

版权声明:本文为博主原创作品,如需转载请标明出处。 https://blog.csdn.net/weixin_42162441/article/details/82807383

REST 的基础知识

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

在 REST 中,资源通过 URL 进行识别和定位。
关注的是实物而不是行为。
有以下的行为以及匹配的 CRUD 动作:

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

Spring 是聚合支持 REST 的

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

  • 控制器可以处理所有的 HTTP 方法,包括四个主要的方法 GET 、 PUT 、 DELETE 、 POST 。
  • 借助 @PathVariable
  • 借助 Spring 的视图和视图解析器,资源能够以多种方式表述,包括将模型数据渲染为 XML 、 JSON 、 Atom 、 以及 RSS 的 View 实现;
  • 可以使用 ContentNegotiatingViewResolver 来选择最适合客户端的表述。
  • 借助 @ResponseBody 和各种 HttpMethodConverter 实现,能够替换基于视图的渲染方式。
  • 类似地,可以使用上条方法将传入的 HTTTP 数据转化为传入控制器处理方法的 Java 对象。
  • 借助 RestTemplate , Spring 应用能够方便地使用 REST 资源。

创建第一个 REST 端点

@Controller
@RequestMapping("/spittles")
public class SpittleController {
	
	private static final String MAX_LONG_AS_STRING="92233720397760";

	private SpittleRepository;

	@Autowired
	public SpittleController(SpittleRepository spittleRepository) {
		this.spittleRepository = spittleRepository;
	}
	@RequestMapping(method=RequestMethod.GET)
	public List<Spittle> spittles(
		@RequestParam(value="max",
						defaultValue=MAX_LONG_AS_STRING) long max,
		@RequestParam(value="count", defaultValue="20") int count) {
		return spittleRepository.findSpittles(max, count);
	}
		
}

需要了解的是控制器本身通常不关心资源如何表述。控制器以 Java 对象的方式来处理资源。控制器完成了它的工作之后,资源才会被转化成最适合客户端的形式。
Spring 提供了两种方法将资源的 Java 表述形式转换为发送给客户端的表述形式:

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

协商资源表述

视图解析方案是个简单的一维活动。当要将视图名解析为能够产生资源表述的视图时,我们就有另外一个维度需要考虑了。视图不仅要匹配视图名,而且所选择的视图要适合客户端。
Spring 的 ContentNegotiatingViewResolver 特殊的视图解析器,考虑到了客户端需要的内容类型。简单的配置如下:

@Bean
public ViewResolver cnViewResolver() {
	return new ContentNegotiatingViewResolver();
}

内容协商的两个步骤:

  1. 确定请求的媒体类型;
  2. 找到适合请求媒体类型的最佳视图。

确定请求的媒体类型

ContentNegotiatingViewResolver 将会考虑到 Accept 头部信息,并使用它所请求的媒体类型,但是它首先查看 URL 的文件扩展名。

如果 URL 在结尾处有文件扩展名的话, ContentNegotiatingViewResolver 将会基于扩展名确定所需的类型。

如果不能从扩展名得到任何媒体类型,就会考虑请求中的 Accept 头部信息。

如果头部信息也没有的话,将会使用 “ / ” 默认的内容类型。

内容类型确定之后, ContentNegotiatingViewResolver 就将逻辑视图名解析为渲染模型的 View 。但 ContentNegotiatingViewResolver 本身不会解析视图。而是委托给其他的视图解析器,让他们来解析视图。

ContentNegotiatingViewResolver 要求其他的视图解析器将逻辑视图名解析为视图。解析后得到的每个视图都会放到一个列表中。这个列表装配完成后,ContentNegotiatingViewResolver 会循环客户端请求的所有媒体类型,在候选的视图中查找能够产生对应内容类型的视图。第一个匹配的视图会用来渲染渲染模型。

影响媒体类型的选择

以上是确定媒体类型的默认策略。通过为其设置一个 ContentNegotiationManage ,可以改变它的行为。能做到的如下

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

有三种配置 ContentNegotiationManager 的方法

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

从 Spring 3.2 开始,Content-NegotiatingViewResolver 的大多数 Setter 方法都被废弃了,鼓励通过 Content-NegotiationManager 来进行配置。

使用 XML 配置 ContentNegotiationManager 最有用的是 ContentNegotiationManagerFactoryBean 。使用“ application/json ”作为默认内容类型配置如下:

<bean id="contentNegotiationManager"
	class="org.springfarmework.http.ContentNegotiationManagerFactoryBean"
	p:defaultContentType="application/json">

因为 ContentNegotiationManagerFactoryBean 是 FactoryBean 的实现,所以它会创建一个 ContentNegotiationManager bean。这个 bean 能够注入到 ContentNegotiatingViewResolver 的 contentNegotiationManager 的属性中。

如果使用 Java 配置的话,获得 ContentNegotiationManager 的最简便方法就是实现 WebMvcConfigurer 接口并重载 configureContentNegotiation() 方法。之前 WebConfig 已经扩展过,所以此处需要做的就是重载 configureContentNegotiation() 方法。如下是一个实现,设置了默认的内容类型:

@Override
public void configureContentNegotiation(
		ContentNegotiationConfigurer configurer ){
	configurer.defaultContentType(MediaType.APPLICATION_JSON);
}

方法中给定了一个 ContentNegotiationConfigurer 对象。ContentNegotiationConfigurer 中的一写方法对应于 ContentNegotiationManager 的 Setter 方法,这样我们就能在 ContentNegotiation-Manager 创建时,设置任意内容协商相关的属性。

接下来,将 ContentNegotiationManager bean 注入到 ContentNegotiatingViewResolver 的 contentNegotiationManager属性中。稍微修改之前的声明

@Bean
public viewResolver cnViewResolver(ContentNegotiationManager cnm) {
	ContentNegotiatingViewResolver cnvr = 
		new ContentNegotiatingViewResolver();
	cnvr.setContentNegotiationManager(cnm);
	return cnvr;
}

接下来是一个简单的实例,更多的内容还需要学习

@Bean
public ViewResolver cnViewResolver(ContentNegotiationManager cnm) {
	ContentNegotiatingViewResolver cnvr =
		new ContentNegotiatingViewResolver();
	cnvr.setContentNegotiationManager(cnm);
	return cnvr;
}

@Override
public void configureContentNegotiation(
		ContentNegotiationConfigurer configurer) {
	configurer.defaultContentType(MediaType.TEXT_HTML);
}

@Bean
public ViewResolver beanNameViewResolver() {
	return new BeanNameViewResolver();
}

@Bean
public View Spittles() {
	return new MappingJackson2JsonView();
}

ContentNegotiatingViewResolver 只能决定资源该如何渲染到客户端,并没有涉及到客户端要发送什么样的表述给控制器使用。
另一个小问题,所选中的 View 会渲染模型给客户端,而不是资源。

使用 HTTP 信息转换器

消息转换 ( message conversion )能够将控制器产生的数据转换为服务于客户端的表述形式。使用消息转换功能时, DispatcherServlet 不再需要将模型数据传送到视图中。没有模型,没有视图,只有产生的数据,以及消息转换器( message converter )转换数据之后产生的资源表述。


正常情况下,当处理方法返回 Java 对象时,这个对象会放在模型中并在视图中渲染使用。如果使用了消息转换功能,需要告诉 Spring 跳过正常的模型/视图流程,并使用消息转换器。做到这一点,最简单的方法是为控制器方法添加 @ResponseBody 注解。
重新修改 spittles() 方法,将方法换回的 List 转换为响应体。

@RequestMapping(method=RequestMethod.GET,produces="application/json")
public @ResponseBody List<Spittle> spittles(
		@RequestParam(value="max",defaultValue=MAX_LONG_AS_STRING) long max,
		@RequestParam(value="count", defaultValue="20") int count) {
	return spittleRepository.findSpittles(max, count);
}

@ResponseBody 注解通知 Spring,将返回的对象作为资源发送给客户端,并将其转换为客户端可接受的表述形式。更具体的,DispatcherServlet 将会考虑到请求中 Accept 头部信息,并查找能够为客户端提供所需表述形式的消息转换器。

Jackson 默认会使用反射,因此如果重构 Java 类型,产生的 JSON 也会变化,为了防止客户端出错,可以在 Java 类型上使用 Jackson 的映射注解。

这里使用 produces 属性表明这个方法只处理预期输出为 JSON 的请求。


在请求体中接受资源状态


@RequestBody 能够告诉 Spring 查找一个消息转换器,将来自客户端的资源表述转换为对象。下面是一个实例:

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

如果忽略注解,saveSpittle() 是一个简单的方法。它接受一个 Spittle 对象作为参数,并使用 SpittleRepository 进行保存,最终返回 spittleRepository.save() 方法所得到的 Spittle 对象。
但是通过注解, @RequestMapping 表明它只能处理”/spittles“的 POST 请求。POST 请求体中预期要包含一个 Spittle 的资源表述。因为 Spittle 参数上使用了 @RequestBody ,所以 Spring 将会查看请求中的 Content-Type 头部信息,并查找能够将请求体转换为 Spittle 的消息转换器。
consumes 属性,设置为“ application/json ”。会关注请求的 Content-Type 头部信息,告诉 Spring 这个方法只处理对 “/spittles” 的 POST 请求,并要求 Content-Type 头部信息为 " application/json"。否则交由其他控制器处理。

为控制器默认设置消息转化

如果编写的控制器有多个方法,并且每个方法都需要信息转换功能的话,下面的方式更优。
@RestController 注解。在控制器类上使用 @RestController 来代替 @Controller 的话, Spring 将会为该控制器的所有处理方法应用消息转换功能。就不必为每个方法都添加 @ResponseBody 了。示例如下:

@RestController
@RequestMapping("/spittles")
public class SpittleController {
	private static final String  MAX_LONG_AS_STRING = "89429374923834223";
	private SpittleRepository spittleRepository;
	@Autowired
	public SpittleController(SpittleRepository spittleRepository) {
		this.spittleRepository = spittleRepository;
	}
	@RequestMapping(method=RequestMethod.GET)
	public List<Spittle> spittles(
	@RequestParam(value="max" defaultValue=MAX_LONG_AS_STRING) long max,
	@RequestParam(value="count", defaultValue="20") int count) {
		return spittleRepository.findSpittles(max, count);
	}
	@RequestMapping(method=RequestMethod.POST, consumes="application/json")
	public Spittle saveSpittle(@RequestBody Spittle spittle) {
		return spittleRepository.save(spittle);
	}
}

发送错误信息到客户端

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

使用多种方式来处理 Spittle 无法找到的场景。

作为 @ResponseBody 的替代方案,控制器方法可以返回一个 ResponseEntity 对象。其中包含响应相关的元数据(如头部信息和状态码)以及要转换成资源表述的对象。
新版本的 spittleById(),它会返回 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 ResponseEntity<Spittle>(spittle, status);
}

@ResponseBody 除了包含响应头信息,状态码以及负载以外,ResponseEntity 还包括了 @ResponseBody 的语义,因此负载部分会渲染到响应体中,所以没有必要在方法上使用 @ResponseBody 注解了。


另外,还可以定义一个包含错误信息的 Error 对象,

public class Error {
	private int code;
	private String message;
	public Error(int code, String message) {
		this.cod = code;
		this.message = message;
	}
	public int getCode() {
		return code;
	}
	public String getMessage() {
		return message;
	}
}

然后,修改 spittleById() ,让它返回 Erroe;

@RequestMapping(value="/{id}",method=RequestMethod.GET)
public ResponseEntiry<?> 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 Rrror(4,"Spittle["  + spittleId + "]not found" );
	return new ResponseEntity<Error>(error ,HttpStatus.MOT_FOUND);

}

@EcxeptionHandler 注解能够用在控制器方法中,用来处理特定的异常。这里表明如果在控制器的任意处理方法中抛出 SpittleNotFoundException 异常,就会调用 SpittleNotFound() 方法来处理异常。至于 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);
}

优点:除了对返回值进行 null 检查,完全关注与成功的场景,同时,在返回类型中,可以移除掉奇怪的泛型。
另外由于知道返回将是 Spittle 并且 HTTP 状态码始终会是200,所以可以用 @ResponseBody 替换 @ResponseEntity。当然,如果控制器类上使用了 @RestController 甚至不再需要 @ResponseBody。


由于 spittleNotFound() 方法始终返回 Error,通过添加 @ResponseStatus(HttpStatus.NOT_FOUND) 注解,来代替,并可以同上移除掉 @ResponseBody :

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

在响应中设置头部信息

应用场景:saveSpittle() 产生一个新对象,并把资源的 URL 放在响应的 Location 头部信息中,并返回给客户端。

@RequestMapping(
	method=RequestMethod.POST
	consumes="application/json")
public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle) {
	Spittle spittle = spittleRepository.save(spittle);
	
	HttpHeaders headers = new HttpHeaders(); //设置 Location 头部信息
	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;
}

Spring 提供了 UriComponentBuilder ,方便构建 URL 。

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

处理器方法所得到的 UriComponentBuilder 中,会预先配置已知的信息如 host、端口以及Servlet 内容。会从处理器方法所对应的请求中获取这些基础信息。
路径的构建分为两步。第一步调用 path() 方法,第二步调用 build() 方法。

猜你喜欢

转载自blog.csdn.net/weixin_42162441/article/details/82807383