创建REST服务
随着客户端的可选方案越来越多,我们让服务器公开API,通过这种API,各种客户端都能与后端功能进行交互,也就是我们所熟知的前后端分离。
编写REST控制器
从服务器中检索数据
@RestController
@RequestMapping(path = "/design",produces = "application/json")
@CrossOrigin(origins = "*")
public class DesignTacoController {
private TacoRepository tacoRepo;
@Aotuwired
EntityLinks entityLinks;
public DesignTacoController(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
}
@GetMapping("/recent")
public Iterable<Taco> recentTacos() {
PageRequest page = PageRequest.of(
0,12,Sort.by("CreateAt").descending()
);
return tacoRepo.findAll(page).getContent();
}
}
@RestController 注解有两个目的,首先,它是一个类似于@Controller和@Service的构造性注解,能够让类被组件扫描功能发现。最重要的是,@RestController注解会告诉Spring,控制器中的所有处理器方法的返回值都要直接写入响应体中,而不是将值放到模型中并传递给一个视图以便于进行渲染。
上述注解也有一个替代方案,就是将@RestController替换成@Controller,但是我们就需要为每个处理器方法再添加一个@ResponseBody注解。另一种替代方案就是返回ResponseEntity对象。
@RequestMapping 注解还设置了一个process属性。这是指明该控制器的所有方法只会处理Accept头信息包含“application/json”的请求。它不仅会限制API只会生成JSON结果,同时还允许其他的控制器处理具有相同路径的请求,只要这些请求不要求JSON格式的输出就可以。我们还可以将process属性设置为一个String类型的数组,这样的话就允许我们设置多个内容类型。比如,为了允许生成XML格式的输出,我们可以将process属性添加“text/xml”:
@RequestMapping(path = “/design”,process = {“application/json”,“text/xml”})
@CrossOrigin 注解,在本控制器中将会允许来自任何域的客户端消费该API。
@GetMapping("/{id}")
public Taco tacoById(@PathVariable("id") Long id)
请求路径的“{id}”部分是占位符。请求中的实际值将会传递给id参数,它通过@PathVariable注解与{id}占位符进行匹配。
return new ResponseEntity<>(null,HttpStatus.NOT_FOUND);
返回一个ResponseEntity对象,包装了一个null,并且带有NOT FOUND的HTTP状态。
发送数据至服务器端
@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public Taco postTaco(@RequestBody Taco taco) {
return tacoRepo.save(taco);
}
consumes属性用于指定请求输入,表明本方法只会处理Content-type与application/json相匹配的请求
@ResponseBody注解表明请求应该被转换为一个Taco对象并绑定到该参数上。@ResponseBody注解确保请求体中的JSON会被绑定到Taco对象上。
@ResponseStatus(HttpStatus.CREATED) 告诉客户端,请求不仅成功了,还创建了一个资源。
在服务器上更新数据
PUT真正的目的是执行大规模的替换操作,而不是更新操作。HTTP PATCH的目的是对资源数据打补丁或局部更新。
删除服务器上的数据
删除之前资源是否存在并不重要,但是也可以让方法返回ResponseEntity,在资源不存在时将响应体设置为null并将HTTP状态码设置为NOT FOUND。
@ResponseStatus(code = HTTPStatus.NO_CONTENT)将响应体状态码为204(NO CONTENT)让客户端知道不要期望得到任何内容。
启用超媒体功能
超媒体作为应用状态引擎(Hypermedia as the Engine of Application State,HATEOAS)是一种创建自描述API的方式。API所返回的资源中会包含相关资源的链接,客户端只需要了解最少的API URL信息就能够导航整个API。这种方式能够掌控API所提供的资源之间的关系,客户端能够基于API的URL中所发现的关系,对他们进行遍历。
如果服务器的返回体中嵌入超链接,这种特殊风格的HATEOAS被称为HAL(超文本应用语言,Hypertext Application Language)。这是一种在JSON相应体中嵌入超链接的简单通用格式。
Spring HATEOAS项目为Spring提供了超链接的支持。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
只要我们重构控制器,使其返回资源类型,而不是领域类型。
添加超链接
@GetMapping
public EntityModel<Optional<Ingredient>> test() {
Optional<Ingredient> employee = ingredientRepository.findById(1);
return EntityModel.of(employee,
linkTo(methodOn(TestController.class).test()).withSelfRel());
}
上述代码运行后你可以看到这样的结果
{
"id": 1,
"name": "热狗",
"_links": {
"self": {
"href": "http://localhost:8080/test"
}
}
}
与之作为对比的有:
@GetMapping
public Optional<Ingredient> get() {
return ingredientRepository.findById(1);
}
以及结果
{
"id": 1,
"name": "热狗"
}
不难发现,元素包含了一个名为"_links"的属性,为客户端提供导航的超链接。self链接用来引用该资源。
这样做的一个好处就是如果客户端需要对该资源进行HTTP请求,那么在开发的时候就不需要关心该资源的URL长什么样子,相反,只需要请求“self”链接就可以了。
Spring HATEOAS提供了两个主要的类型来表示超链接资源:EntityModel和CollectionModel,EntityModel代表一个资源,而CollectionModel代表一个资源的集合。这两种类型都能携带到其他资源的链接,当从Spring MVC REST控制器返回时,他们所携带的链接将会包含到客户端接收到的JSON中。
linkTo与methodOn()方法来自WebMvcLinkBuilder,如果使用Spring WebFlux则必须改用WebFluxLinkBuilder。
@PostMapping
public CollectionModel<EntityModel<Ingredient>> test2() {
List<Ingredient> ingredients = (List<Ingredient>) ingredientRepository.findAll();
CollectionModel<EntityModel<Ingredient>> resources = CollectionModel.wrap(ingredients);
return CollectionModel.of(resources,
linkTo(methodOn(TestController.class).test2()).withSelfRel());
/*resources.add(
linkTo(methodOn(TestController.class).test2()).withSelfRel()
);*/
/*return resources;*/
}
上述为CollectionModel的用法结果为
{
"_embedded": {
"ingredientList": [
{
"id": 1,
"name": "热狗"
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/test"
}
}
}
创建装配器
我们需要为我们的领域数据添加链接,如此我们就可以创造一个领域数据资源,创建一个单独的资源类,从而将id属性排除出去,以及不必要为每个领域都添加链接。
public class TacoResource extends RepresentationModel<TacoResource> {
@Getter
private String name;
@Getter
private Date createdAt;
@Getter
private List<Ingredient> ingredients;
public TacoResource(Taco taco) {
this.name = taco.getName();
this.createdAt = taco.getCreatedAt();
this.ingredients = taco.getIngredients();
}
}
为了将领域对象转换成领域资源对象,我们需要创建一个资源适配器
public class IngredientResourceAssembler extends RepresentationModelAssemblerSupport<Ingredient, IngredientResource> {
public IngredientResourceAssembler(Class<?> controllerClass, Class<IngredientResource> resourceType) {
super(controllerClass, resourceType);
}
@Override
protected IngredientResource instantiateModel(Ingredient entity) {
return new IngredientResource(entity);
}
@Override
public IngredientResource toModel(Ingredient entity) {
return createModelWithId(entity.getId(),entity);
}
}
IngredientResourceAssemble有一个默认的构造器,会告诉超类在创建IngredientResource中的链接时,将使用该控制器来确定所有URL的基础路径。
instantiateModel方法进行了重写,以便于给定的Ingredient实例化IngredientResource。如果IngredientResource有默认构造器,那么这个方法是可选的。
ToModel方法是用于告诉它通过Ingredient来创建IngredientResource,并设置一个self链接,并且这个链接的URL是根据Ingredient对象的id属性衍生出来的
在内部,ToModel将会调用instantiateModel。
控制器的调整
@GetMapping("/test")
public CollectionModel<IngredientResource> ingredients () {
List<Ingredient> ingredients = (List<Ingredient>) ingredientRepository.findAll();
CollectionModel<IngredientResource> ingredientResources =
new IngredientResourceAssembler(
TestController.class
,IngredientResource.class)
.toCollectionModel(ingredients);
ingredientResources.add(linkTo(methodOn(
TestController.class)
.ingredients())
.withSelfRel());
return ingredientResources;
结果:
{
"_embedded": {
"ingredientResourceList": [
{
"name": "热狗",
"_links": {
"self": {
"href": "http://localhost:8080/test/1"
}
}
},
{
"name": "生菜",
"_links": {
"self": {
"href": "http://localhost:8080/test/2"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/test/test"
}
}
}
如果是资源中嵌套资源的情况,如Taco与Ingredients,则需要这样定义上层资源:
public class TacoResource extends RepresentationModel<TacoResource> {
private static final IngredientResourceAssembler
ingredientAssembler = new IngredientResourceAssembler(IngredientController.class, IngredientResource.class);
@Getter
private String name;
@Getter
private Date createdAt;
@Getter
private CollectionModel<IngredientResource> ingredients;
public TacoResource(Taco taco) {
this.name = taco.getName();
this.createdAt = taco.getCreatedAt();
this.ingredients = ingredientAssembler.toCollectionModel(taco.getIngredients());
}
}
@Relation注解能够帮助我们消除JSON字段名,和Java代码中定义的资源名的耦合
@Relation(value = "taco",collectionRelation = "tacos")
public class TacoResource extand ···
启用后端数据服务
为了在资源中添加超链接,也可以借助Spring Data REST自动创建API。需要用Spring Data来实现repository。
为了使用Spring Data REST,添加依赖:
<dependency>
<groupId>org.springframework.boot</goupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</denpendency>
为了避免Spring Data REST 创建的API与自己创建的控制器发生冲突,我们可以为其API设置基础路径。设置spring.data.rest.base-path属性。
spring:
data:
rest:
base-path: /api
这项配置会将Spring Data REST端点的基础路径设置为“/api”
请求资源时,可能遇到因为资源其名的复数形式而出现问题。如taco->tacoes
我们可以使用@RestResource为我们提供任何想要的关系名以及路径。只要将其注解在entity类名前。
@RestResource(rel = "tacos",path = "tacos")
分页及排序
Spring Data REST提供分页支持,要使用它,repository需要继承PagingAndSortingRepository<>,而不是CurdRepository。我们可以在URL出设置分页大小如L:
http://localhost:8080/api/ingredients?page=0&size=1
展现结果就有趣得多
{
"_embedded": {
"ingredients": [
{
"name": "热狗",
"_links": {
"self": {
"href": "http://localhost:8080/api/ingredients/1"
},
"ingredient": {
"href": "http://localhost:8080/api/ingredients/1"
}
}
}
]
},
"_links": {
"first": {
"href": "http://localhost:8080/api/ingredients?page=0&size=1"
},
"self": {
"href": "http://localhost:8080/api/ingredients?page=0&size=1"
},
"next": {
"href": "http://localhost:8080/api/ingredients?page=1&size=1"
},
"last": {
"href": "http://localhost:8080/api/ingredients?page=1&size=1"
},
"profile": {
"href": "http://localhost:8080/api/profile/ingredients"
}
},
"page": {
"size": 1,
"totalElements": 2,
"totalPages": 2,
"number": 0
}
}
page参数是从0开始的。
不仅可以分页,还可以将分页与排序混合起来。如按照降序排序:
http://localhost:8080/api/ingredients?sort=id,desc&page=0&size=1
创建自定义端点
Spring Data REST为我们提供了一个新的注解@RepositoryRestController,这个注解可以用到控制器类上,这样控制器类所有映射得基础路径就会与Spring Data REST端点配置的基础路径相同。
@RepositoryRestController并不能保证处理器方法的值会自动写入响应体中,所以我们要么为方法添加@ResponseBody注解,要么返回包装响应数据的@ResponseEntity。
为Spring Data端点添加自定义的超链接
通过声明资源处理器bean,我们可以为Spring Data REST自动包含的链接列表继续添加链接。Spring Data HATEOAS提供了一个RepresentationModelProcessor接口,能够在资源通过API返回自前对其进行操作。
如“api/tacos”端点所返回的类型为PagedModel<EntityModel>添加链接。Spring HATEOAS会自动发现这个bean并将其应用到对应资源上。如果控制器返回PagedModel<EntityModel>,就会包含一个最新创建的taco链接。
@Bean
public RepresentationModelProcessor<PagedModel<EntityModel<Taco>>> tacoProcessor(EntityLinks links) {
return new RepresentationModelProcessor<PagedModel<EntityModel<Taco>>>() {
@Override
public PagedModel<EntityModel<Taco>> process(PagedModel<EntityModel<Taco>> resource) {
resource.add(
links.linkFor(Taco.class)
.slash("recent")
.withRel("recents"));
return resource;
}
};
}
.slash会为URL添加斜线。