REST services
编写RESTful风格的controller
SpringMVC 提供了@RequestMapping
注解用于映射请求与对应的Controller方法。同时,还提供了以下几个更加详细的注解:
Annotation | HTTP method | Typical use |
---|---|---|
@GetMapping | HTTP GET requests | Reading resource data |
@PostMaping | HTTP POST requests | Creaating a resource |
@PutMapping | HTTP PUT requests | Updating a resource |
@PatchMapping | HTTP PATCH requests | Updating a resource |
@DeleteMapping | HTTP DELETE requests | Delete a resource |
也可以通过@RequestMapping
的method属性来指定HTTP方法。
通过服务获取数据
@RestController
@RequestMapping(path="/design",produces="application/json")
@CrossOrigin(origins="*")
public class DesignTaacoController{
...
@GetMapping("recent")
public Itarable<Taco> recentDesigns(){
...
return designs;
}
}
上面的Controller与普通的Controller的不同点:
- 使用的是
@RestController
而非Controller
@RestController
第一个作用是使被注解的类被Spring容器探测到从而注册为bean;第二个作用告诉Spring被注册的类中的所有请求处理方法的返回值都直接写入响应体,而不是通过Model
传递给页面。
我们也可以再类级别使用@Controller
注解,在方法级别使用@ResponseBody
注解达到同样的效果。
@RequestMapping
多了produces
参数
这里的produces
值为"application/json",这表明该类中的所有方法只响应那些请求头(Header)中Accept
属性值为"application/json"的请求;同时也限制了该类的处理方法的返回值全部都为json格式;另外它还允许其它的Controller来处理相同URL请求(只要请求不要求json格式的结果)。
我们也可以指定多个数据返回格式,如:RequestMapping(path="/design",produces={"application/json","text/xml"})
。我们只需要加入XML的依赖包就可以实现返回XML格式的数据:
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-xml-provider</artifactId>
<version>2.5.0</version>
</dependency>
- 使用了
@CrossOrigin
注解
作为一个前后端分离的项目,前端页面与后端服务可能不在同一个host和端口上,这种情况下浏览器会阻止前端调用服务。我们可以通过在响应头天骄CORS来消除这一限制。Spring 则通过使用@CrossOrigin
注解来实现。这里的origins
属性值为 “*” ,表示该服务允许来自所有源的请求。
向服务传递查询参数
通过id查询Taco
@GetMapping("/{id}")
public ResponseEntity<Taco> tacoById(@PathVariable long id){
Optional<Taco> optTaco = tacoRepository.findById(id);
if (optTaco.isPresent()){//查询到了结果
return new ResponseEntity<>(optTaco.get(),HttpStatus.OK);
}else{
return new ResponseEntity<>(null,HttpStatus.NOT_FOUND);
}
}
这个方法与recentDesigns
方法的差别:
-
@GetMapping("/{id}")
:
使用了占位符{}
,spring会将URL中{id}
部分的值作为查询参数,并将值赋给被@PathVarialbe
注解的入参id
。 -
@PathVariable
:
该注解有3个属性:
- name: 指定绑定URL中的那个变量值,默认为"",
- value: 当无法从URL中找到变量值时,使用该属性值作为默认值。该属性值默认为""。
- required:如URL中没有找到对应值,将抛出异常。默认为
true
。
- 返回
ResponseEntity
对象
使用改对象,我们可以根据处理结果设置请求结果状态码。如上,查询到了记录,状态码为HttpStatus.OK
;没有查到数据,状态码为:HttpStatus.NOT_FOUND
。更多状态码
向服务发送数据
@PostMapping(consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public Taco postTaco(@RequestBody Taco taco){
return tacoRepository.save(taco);
}
-
PostMapping(consumes="application/json")
:
指定该方法只处理请求头中,Content-type=application/json
的请求。 -
@ResponseStatus(HttpStatus.CREATED)
:
当该方法正常结束时,响应头中的结果状态码为201。改状态码不仅表示请求成功,还表示该请求创建了新的资源。 -
@RequestBody
:
这个注解非常重要,如果没有这个注解,那么SpringMVC会猜测我们是想要将查询参数或者是表单提交的数据赋值给taco。使用该注解,那么请求体(Request Body) 中的JSON数据将会被绑定给taco。
更新数据
我们有两种方式更新数据,一种是基于HTTP PUT
请求,一种是基于HTTP PATCH
请求。两种方式的区别在于:
HTTP PUT
:用于替换整个资源HTTP PATCH
:用于更新部分资源属性。
整个两个请求方法只是根据其语义来区分功能的,具体的更新方式还是看我们如何编码实现。下面是简单的实现:
@PutMapping(consumes="application/json")
public Taco update(@ResponseBody Taco taco){
return tacoRepository.save(taco);
}
@PatchMapping(path="/{id}",consumes="application/json")
public Taco update(@PathVariable long id, @ResponseBody Taco taco){
Taco oldTaoc = tacoRepository.findById(id);
if (taco.getName() != null){
oldTaco.setName(taco.getName());
}
...
...
}
删除数据
@DeleteMapping("/{id}")
@ResponseStatus(code=HttpStatues.NO_CONTENT)
public void deleteOrder(@PathVariable("id") long id){
tacoRepository.deleteById(id);
}
删除成功后返回的状态码为HttpStatus.NO_CONTENT
(204),删除后不需要返回任何数据给客户端,所以返回204给客户端,告诉客户端响应体体中没有响应数据。
使用超媒体(Hypermedia)
Hypermedia(一种应用描述引擎)或 HATEOAS,提供了一种手段,它可以在API返回的资源中创建描述资源资源本身的API(即资源的URL)。
HATEOAS(Hypermedia as the Engine of Application State),也被称为 HAL(Hypertext Application Language),是一种简单的被普遍使用的向JSON中插入超链接的格式。
Spring HATEOAS项目提供了一些了的类和资源整合器,可以在SpringMVC Controller返回资源之前,向资源中添加超链接。使用HATEOAS,就可以避免在客户端硬编码资源路径或者操作资源路径字符串,而是直接使用接口返回的资源路径。
要使用 Spring HATEOAS 就需要加入其依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
添加超链接
Spring HATEOS 提供了两个类:CollectionModle
和EntityModel
,这两个类可以实现向资源中加入超链接。从类名就可以分辨出,CollectionModle
用于向集合资源添加超链接;EntityMoel
用于向单个的对象资源添加超链接。
@GetMapping("/recent")
public CollectionModel<Taco> recentDesigns(){
PageRequest page = PageRequest.of(0,12,Sort.by("createAt"));
List<Taco> tacos = tacoRepository.findAll(page).getContent();
CollectionModel<EntityModel<Taco>> recentCollection = CollectionModel.wrap(tacos);
recentCollection.add(new Link("http://localhost:8080/design/recent"),"recents");
return recentCollection;
}
-
返回值不是
List<Taco>
,而是CollectionModel<Taco>
,使用CollectionModel.wrap()将查询结果封装起来。 -
调用了
CollectionModel
的add
方法,为List<Taco>
资源定义了URL及URL名称
上面的例子我们还是通过硬编码的方式定义的URL,下面我们使用更好的方式来构造URL:
recentCollection.add(WebMvcLinkBuilder.linkTo(DesignController.class).slash("recent").withRef("recents"));
linkTo(DesignController.class)
:WebMvcLinkBuilder使用DesignController的基本路径作为正在创建的Link的基本路径。slash("recent")
:顾名思义,在构建的Link对象的URL后加上/recent
withRef("recents")
:给该URL提供一个相关的名称。
上面的方式还是存在少部分的URL硬编码,使用methodOn
我们可以消除所有URL相关的硬编码:
recentCollection.add(WebMvcLinkBuilder.linkTo(methodOn(DesignController.class).recentDesigns()).withRef(recents));
通过上面的方法,WebMvcLinkBuilder使用DesignController
的基本路径加上recentDesigns
方法的路径组成了URL。
创建资源组装器
前面我们返回的资源中,只有List<Taco>
列表的URL,列表类的每个Taco
并没有URL,我们需要为每个Taco
也加上URL。
- 我们需要为
Taco
类定义一个通用类
@Data
public class TacoModel extends RepresentationModel<TacoModel>{
private Date createAt;
private String name;
private List<Ingredient> ingredients;
}
这个通用类没有Id字段,那是因为我们不需要展示它。在客户端API中,资源自身的URL将会作为资源的唯一标识。
- 我们还需要一个资源组装器,用于将
Taco
列转换为TacoModel
:
public class TacoModelAssembler extends RepresentationModelAssemblerSupport <Taco,TacoModel> {
public TacoModelAssembler() {
super(DesignTacoController.class,TacoModel.class);
}
@Override
public TacoModel toModel(Taco entity) {
TacoModel tacoModel = createModelWithId(entity.getId(),entity);
tacoModel.setCreateAt(entity.getCreateAt());
tacoModel.setIngredients(entity.getIngredients());
tacoModel.setName(entity.getName());
return tacoModel;
}
}
- 构造方法:
构造方法调用了父类的构造方法,在上面的例子中,当构造TacoModel
实例时,将会使用DesignController
来决定Taco的基本URL。
createModelWithId()
:
调用该方法,会返回一个TacoModel
实例,且这个实例中已经加入了对应entity的URL。
- 将
List<Taco>
转换为CollectionModel<TacoModel>
:
public CollectionModel<TacoModel> recent(){
List<Taco> tacos = tacoRepository.findAll();
CollectionModel<TacoModel> tacoModels = new TacoModelAssemeler().toCollectionModel(tacos);
tacoModels.add(WebMvcLinkBuilder.linkTo(methodOn(DesignController.class).recent()).withRef("recents"));
}
因为TacoModel
中存在List<Ingredient>
域,若只是做了以上工作,那么每个Taco
对象中的Ingredient
域并不包含URL,所以我们还要为Ingredient
添加组装器:
-
Ingredient
的通用类和组装器实现跟Taco
类似 -
需要对
Taco
的通用类和组装器做一点改变:
-
TacoModel
:原本是ingreds域的类型为:List<Ingredient>
,现在要改为:CollectionModel<IngredientModel>
: -
TacoModelAssembler
:
@Override
public TacoModel toModel(Taco entity) {
TacoModel tacoModel = createModelWithId(entity.getId(),entity);
tacoModel.setCreateAt(entity.getCreateAt());
tacoModel.setIngredients(new IngredentModelAssembler(IngredientController.class,IngredientModel.class).toCollectionModel(entity.getIngredients()));
tacoModel.setName(entity.getName());
return tacoModel;
}
给资源命名
在前面的例子中,返回的jsong格式可能是这样的:
{
"_embedded": {
"tacoModelList": [
{
"createAt": "2020-04-17T14:39:59.000+0000",
"name": "taco1",
"ingredients": {
"_embedded": {
"ingredientModelList": [
{
"name": "Flour Tortilla",
"type": "WRAP",
"_links": {
"self": {
"href": "http://localhost:8080/rest/ingredient/FLTO"
}
}
},
{
"name": "Lettuce",
"type": "VEGGIES",
"_links": {
"self": {
"href": "http://localhost:8080/rest/ingredient/LETC"
}
}
}
]
}
},
"_links": {
"self": {
"href": "http://localhost:8080/rest/design/3"
}
}
},
{
"createAt": "2020-04-17T14:42:38.000+0000",
"name": "taco2",
"ingredients": {
"_embedded": {
"ingredientModelList": [
{
"name": "Flour Tortilla",
"type": "WRAP",
"_links": {
"self": {
"href": "http://localhost:8080/rest/ingredient/FLTO"
}
}
},
{
"name": "Ground Beef",
"type": "PROTEIN",
"_links": {
"self": {
"href": "http://localhost:8080/rest/ingredient/GRBF"
}
}
}
]
}
},
"_links": {
"self": {
"href": "http://localhost:8080/rest/design/4"
}
}
}
]
},
"_links": {
"recents": {
"href": "http://localhost:8080/rest/design/recent"
}
}
}
CollectionModel<TacoModel>
在json中的名称为:tacoModelList,CollectionModel<IngredientModel>
在json中的名称为:ingredientModelList。我们可以修改这些名称
@Data
@Relation(value="taco",collectionRelation = "tacos")
public class TacoModel extends RepresentationModel<TacoModel>{
private Date createAt;
private String name;
private CollectionModel<IngredientModel> ingredients;
}
使用@Relation(value="taco",collectionRelation = "tacos")
注释通用类,当返回TacoModel
列表时,列表名称为tacos,返回单个TacoModel
对象时,名称为taco。
使用以数据为中心的服务
Srping Data REST可以自动为那些基于Spring Data的Repository创建REST API。我们唯一需要做的就是在项目中加入它的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
我们定义了一个这样的Repository:
public interface IngredientRepository extends CrudRepository<Ingredient,String>{
}
我们就可以发出一个向http://localhost:8080/ingredients
的GET请求,该服务将返回所有的Ingredient,并且还带有资源的超链接:
{
"_embedded": {
"ingredients": [
{
"name": "Carnitas",
"type": "PROTEIN",
"_links": {
"self": {
"href": "http://localhost:8080/ingredients/CARN"
},
"ingredient": {
"href": "http://localhost:8080/ingredients/CARN"
}
}
},
...
]
},
"_links": {
"self": {
"href": "http://localhost:8080/ingredients"
},
"profile": {
"href": "http://localhost:8080/profile/ingredients"
}
}
}
为了让这些接口的URL与普通请求的URL分开,我们可以设置API的基本路径:
spring:
data:
rest:
base-path: /api
这样,获取所有ingredient的URL就变为了:http://localhost:8080/api/ingredinets
与Ingredient相同, 我们也定义了TacoRepository
接口,但是访问http://localhost:8080/api/tacos却是404:
调整资源路径与关联名称
Spring Data REST 使用相关实体的 复数形式 来组成URL。但是对于Taco
实体,却使用的是错误的复数形式:“tacoes”,而不是"tacos"。所以为了确认所有暴露的API链接,我们可以向API的基础路径:http://localhost:8080/api 发出GET请求:
{
"_links": {
"tacoes": {
"href": "http://localhost:8080/api/tacoes{?page,size,sort}",
"templated": true
},
"users": {
"href": "http://localhost:8080/api/users"
},
"orders": {
"href": "http://localhost:8080/api/orders"
},
"ingredients": {
"href": "http://localhost:8080/api/ingredients"
},
"profile": {
"href": "http://localhost:8080/api/profile"
}
}
}
通过结果我们可以看到,Taco实体的路径和关系名称都是"tacoes"。
除此之外,我们可以在Taco
实体类上添加@RestResource
注解,人为的指定路径和关系名称:
@Data
@Entity
@RestResource(path = "tacos",rel = "tacos")
public class Taco {
...
}
这样,我们就可以通过/api/tacos来请求服务了。
分页与排序
前面我们通过/api 的到的结果可以看到,Taco实体的接口路径后面可以带参数"{?page,size,sort}",这些参数就是为分页和排序提供的。
/api/tacoes?page=0&size=12&sort=createAt,desc:按’createAt’字段降序查询 第1页(页数从0开始),每页大小为12。
要使得API支持分页与排序,那么我们的Repository应该继承PagingAndSortingRepository
接口,该接口是CrudRepository
接口的子接口。
自定义API
至此,我们只有简单的CRUD接口,如果需要复杂功能的接口,还是需要我们在Controller中定义。我们之前使用@RestController
注解Controller创建的API任然可以独立于Spring Data API而被访问,但是这样会有两个问题:
- 我们自己创建的Controll并没有映射到Spring Data REST的根路径下。
- Spring Data REST不会将我们自己创建的Controller作为链接包含在资源中。
我们可以使用@RepositoryRestController
来注解自定义Controller,这样,该Controller的路径将会被映射到Spring Data REST的根路径上。如查询最近设计的Taco:
@RepositoryRestController
public class RecentTacosController {
private TacoRepository tacoRepository;
@Autowired
public RecentTacosController(TacoRepository tacoRepository) {
this.tacoRepository = tacoRepository;
}
@GetMapping("/tacos/recent")
public ResponseEntity<CollectionModel<TacoModel>> recentTacos(){
PageRequest pageRequest = PageRequest.of(0,2, Sort.by("createAt").descending());
Iterable<Taco> tacos = tacoRepository.findAll(pageRequest).getContent();
CollectionModel<TacoModel> collections = new TacoModelAssembler().toCollectionModel(tacos);
if (tacos.iterator().hasNext()){
return new ResponseEntity<CollectionModel<TacoModel>>(collections, HttpStatus.OK);
}
return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
}
}
值得注意的是:虽然该注解域@RestController
相似,但是它的作用并非像其语义一样是一个RestController,它不会保证将处理方法的返回值自动的写入响应体中。所以我们应该使用@ResponseBody
注解处理方法,或者返回ResponseEntity
类型的值。
添加自定义链接到Spring Data API中
虽然我们的自定义Controller已经映射到了Spring Data REST的根路径上,但是Spring Data REST API返回的资源中并没有加入这些路径,我们需要声明一个资源处理器bean:
@Bean
public RepresentationModelProcessor<CollectionModel<Taco>> tacoProcessor(EntityLinks links){
return new RepresentationModelProcessor<CollectionModel<Taco>>() {
@Override
public CollectionModel<Taco> process(CollectionModel<Taco> model) {
model.add(links.linkFor(Taco.class).slash("recent").withRel("recents"));
return model;
}
};
}
通过定义上面的bean,当类型为:CollectionModel<Taco>
的资源被组装 之后(RepresentationModelAssemblerSupport.toModel
或者RepresentationModelAssemblerSupport.toCollection
被调用之后),上面的process方法将会被调用。这样当我们请求:/api/tacos时,返回的数据将会添加我们自定义的Controller路径: