概述
1. 摘要
- 在Spring MVC中实现RESTful API
- Spring HATEOAS
- Spring Data REST
2. REST
REST (REpresentational State Transfer),是一个标准,一种规范,遵循REST风格可以使开发的接口通用,便于调用者理解接口的作用,即使用URL定位资源,用HTTP动词(GET,POST,PUT,DELETE)描述操作
RESTful架构风格一些规定:
数据的元操作(即CURD)分别对应HTTP不同请求:
请求 描述 GET 获取操作 POST 新建资源(也可以用于更新资源) PUT 更新全部资源 PATCH 更新局部资源 DELETE 删除资源 返回对应的状态码来标识不同的接口调用状态,以下部分常见状态码
状态 描述 201 Created 成功创建 202 Accepted 表示服务器端已经收到请求消息,但是尚未进行处理。
但是对于请求的处理确实无保证的,即稍后无法通过
HTTP 协议给客户端发送一个异步请求来告知其请求
的处理结果。这个状态码被设计用来将请求交由另
外一个进程或者服务器来进行处理,或者是对请求进行批处理的情形204 No Content 服务器成功处理了请求,但不需要返回任何实体内容 303 See Other 对应当前请求的响应可以在另一个 URI 上被找到,
而且客户端应当采用 GET 的方式访问那个资源。
这个方法的存在主要是为了允许由脚本激活的POST请求
输出重定向到一个新的资源。400 Bad Request 当前请求无法被服务器理解,如果其他 4xx 的状态码
不适合表示出错原因,再使用此401 Unauthorized 当前请求需要用户验证。该响应必须包含一个适用
于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。
客户端可以重复提交一个包含恰当的 Authorization
头信息的请求。如果当前请求已经包含了 Authorization 证书,
那么401响应代表着服务器验证已经拒绝了那些证书。
如果401响应包含了与前一个响应相同的身份验证询问,
且浏览器已经至少尝试了一次验证,那么浏览器应当
向用户展示响应中包含的实体信息,因为这个实体信
息中可能包含了相关诊断信息。402 Payment Required 此响应码保留以便将来使用,创造此响应码的最初目
的是用于数字支付系统,然而现在并未使用403 Forbidden 服务器已经理解请求,但是拒绝执行它 404 Not Found 请求失败,请求所希望得到的资源未被在服务器上发现 422 Unprocessable Entity 请求格式良好,但由于语义错误而无法遵循 423 Locked 正在访问的资源被锁定 429 Too Many Requests 用户在给定的时间内发送了太多请求(“限制请求速率”) 501 Not Implemented 此请求方法不被服务器支持且无法被处理 这样就统一了数据操作的接口,使用同一个URL不同的请求方式即可完成对数据的增删改查
创建RESTful Controller
Controller
@RestController(value = "StockController") @RequestMapping(path = "/pop" , produces = "application/json") //produces定义响应数据格式 @CrossOrigin(origins = "*") public class PopStockController { private ViewProps viewProps; @Autowired public void setViewProps(ViewProps viewProps) { this.viewProps = viewProps; } private JpaPopInStockRepository popInStockRepository; @Autowired public void setPopInStockRepository(JpaPopInStockRepository popInStockRepository) { this.popInStockRepository = popInStockRepository; } @GetMapping public String show() throws JsonProcessingException { List<PopInStock> list = popInStockRepository.findPopInStocksByOrderByTaskDesc(PageRequest.of(0,viewProps.getPageSize())); return new ObjectMapper().writeValueAsString(list); } //查询数据 @GetMapping("/{id}") public ResponseEntity<PopInStock> getById(@PathVariable("id") int id){ Optional<PopInStock> optPop = Optional.ofNullable(popInStockRepository.findById(id)); return optPop.map(popInStock -> new ResponseEntity<>(popInStock, HttpStatus.OK)) .orElseGet(() -> new ResponseEntity<>(null, HttpStatus.NOT_FOUND)); } //插入数据 @PostMapping(consumes = "application/json") //consumes定义请求数据类型 @ResponseStatus(HttpStatus.CREATED) public void postPop(@RequestBody PopInStock pop){ popInStockRepository.save(pop); } //全部更新 @PutMapping(consumes = "application/json") @ResponseStatus(HttpStatus.NO_CONTENT) public void putPop(@RequestBody PopInStock pop){ popInStockRepository.save(pop); } //部分更新 @PatchMapping(consumes = "application/json",path = "/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void patchPop(@RequestBody PopInStock pop,@PathVariable("id") int id){ PopInStock origin = popInStockRepository.findById(id); if(pop.getTask() != null) origin.setTask(pop.getTask()); if(pop.getCode() != null) origin.setCode(pop.getCode()); if(pop.getName() != null) origin.setName(pop.getName()); if(pop.getVersion() != null) origin.setVersion(pop.getVersion()); if(pop.getBrand() != null) origin.setBrand(pop.getBrand()); if(pop.getInStockCount() != null) origin.setInStockCount(pop.getInStockCount()); if(pop.getComment() != null) origin.setComment(pop.getComment()); popInStockRepository.save(origin); } //删除 @DeleteMapping(path = "/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteById(@PathVariable("id") int id){ try{ popInStockRepository.deleteById(id); }catch (EmptyResultDataAccessException e){} } }
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>RESTful</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> </head> <body> <div id="rest"> <h2>查询</h2> <div> <button v-on:click="retrieve">查询</button> <input v-model="popId" type="text" /> </div> <table> <tr> <td>TASKID</td> <td>CODE</td> <td>NAME</td> <td>BRAND</td> <td>COUNT</td> </tr> <tr v-for="item in items"> <td>{{ item.task }}</td> <td>{{ item.code }}</td> <td>{{ item.name }}</td> <td>{{ item.brand }}</td> <td>{{ item.count }}</td> </tr> </table> <hr/> <h2>新增</h2> <div> <input v-model="createForm.task" type="text" /><br/> <input v-model="createForm.code" type="text" /><br/> <input v-model="createForm.name" type="text"/><br/> <input v-model="createForm.version" type="text"/><br/> <input v-model="createForm.brand" type="text"/><br/> <input v-model="createForm.count" type="text"/><br/> <input v-model="createForm.comment" type="text"/><br/> <button v-on:click="create">新增</button> </div> <h2>全部更新/部分更新</h2> <div> <input v-model="updateForm.id" type="text" /><br/> <input v-model="updateForm.task" type="text" /><br/> <input v-model="updateForm.code" type="text" /><br/> <input v-model="updateForm.name" type="text"/><br/> <input v-model="updateForm.version" type="text"/><br/> <input v-model="updateForm.brand" type="text"/><br/> <input v-model="updateForm.count" type="text"/><br/> <input v-model="updateForm.comment" type="text"/><br/> <button v-on:click="updateAll">全部更新</button> <button v-on:click="updatePart">部分更新</button> </div> <h2>删除</h2> <div> <input v-model="deleteId" type="text" /> <button v-on:click="deleteById">删除</button> </div> </div> <script> new Vue({ el: '#rest', data: { items: null, popId: "", createForm: { task: "", code: "", name: "", version: "", brand: "", count: "", comment: "" }, updateForm: { id: "", task: "", code: "", name: "", version: "", brand: "", count: "", comment: "" }, deleteId: "" }, methods: { retrieve: function(){ axios .get("/pop/"+this.popId).then(response => (this.items = response.data)) .catch(function (error) { console.log(error); }); }, create: function(){ axios.post("/pop",this.createForm).then(response => console.log(response.status)) .catch(function (error) { console.log(error); }); }, updateAll: function(){ axios.put("/pop",this.updateForm).then(response => console.log(response.status)) .catch(function (error) { console.log(error); }); }, updatePart: function(){ axios({ method:'patch', url: "/pop/"+this.updateForm.id, data:this.updateForm }).then(response => console.log(response.status)) .catch(function (error) { console.log(error); }); }, deleteById: function(){ axios.delete("/pop/"+this.deleteId).then(response => console.log(response.status)) .catch(function (error) { console.log(error); }); } } }) </script> </body> </html>
Repository
public interface JpaPopInStockRepository extends CrudRepository<PopInStock,Integer> { List<PopInStock> findPopInStocksByOrderByTaskDesc(Pageable pageable); PopInStock findById(int id); }
domain
@Data @Entity @Table(name = "POP_IN_STOCK") public class PopInStock { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(name = "TASKID") private Integer task; @Column(name = "POP_CODE") private String code; @Column(name = "POP_NAME") private String name; private String version; private String brand; @Column(name = "IN_STOCK_COUNT") @JsonProperty(value = "count") private Double inStockCount; private String comment; }
Spring HATEOAS
- HATEOAS(Hypermedia as the Engine of Application State) 是一种创建字描述API的方法,他在从API中返回资源时为每个资源额外添加该资源的连接
- 对于上面RESTful API我们可以获取资源列表,但是如果获取其中的一个资源,客户端却不知道如何请求,为了使客户端能够在对API了解最少的情况下更好使用,一种较好的方式是返回的JSON中为每个资源添加资源的请求链接
Spring对此提供了支持,使用相应API可以更加简单的实现给返回资源额外添加对应的链接
maven依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency>
1. API变更
以前版本的
Resourse
等相关的类名已经由新的类名替代,使用方法与以前类似,具体列表如下
Resource
⟹EntityModel
Resources
⟹CollectionModel
PagedResources
⟹PagedModel
ResourceAssembler
⟹RepresentationModelAssembler
ResourceSupport
⟹RepresentationModel
2. 使用EntityLinks
配置启用
@Configuration @EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) public class HypermediaConfiguration{ }
使用EntityLinks可以方便的为对象创建连接
EntityLinks links = …; //前提需要在Controller中使用@ExposesResourceFor(Customer.class)注解 LinkBuilder builder = links.linkFor(Customer.class); Link link = links.linkToItemResource(Customer.class, 1L);
配置完成后,系统会默认生成一个``EntityLinks
,即可以在使用的地方自动注入
EntityLinks`的bean@Autowired EntityLinks links;
3. 实例
Controller
@RestController //暴露此类可供其他类使用 //new PopInStockAssembler(PopInStock.class).toCollectionModel(list) @ExposesResourceFor(PopInStock.class) @RequestMapping("/popInStocks") public class PopController { private JpaPopInStockRepository popInStockRepository; private EntityLinks links; @Autowired public void setPopInStockRepository(JpaPopInStockRepository popInStockRepository) { this.popInStockRepository = popInStockRepository; } @Autowired public void setLinks(EntityLinks links) { this.links = links; } @GetMapping("/list") public CollectionModel<PopInStockModel> popInStocks(){ PageRequest page = PageRequest.of(0,2, Sort.by("task")); Link link = null; //link = links.linkToCollectionResource(PopInStock.class).withRel("recents"); link = links.linkFor(PopInStock.class) .slash("/list") //添加额外路径 .withRel("recents"); //设置生成的json字段名 List<PopInStock> list = popInStockRepository.findAll(page); CollectionModel<PopInStockModel> collectionModel = new PopInStockAssembler().toCollectionModel(list);//集合内部对象创建连接 collectionModel.add(link); //添加链接 return collectionModel; } @GetMapping("{id}") public EntityModel<PopInStock> popInStock(@PathVariable int id){ Link link = links.linkToItemResource(PopInStock.class,id) .withRel("popInStock"); //给对象创建连接 EntityModel<PopInStock> entityModel = new EntityModel<>(popInStockRepository.findById(id)); entityModel.add(link); //添加链接 return entityModel; } }
Assembler: 可以自动为集合内部对象增加链接
public class PopInStockAssembler extends RepresentationModelAssemblerSupport<PopInStock, PopInStockModel> { public PopInStockAssembler() { super(PopController.class,PopInStockModel.class); } //重写: 将对象转换为model对象 @Override public PopInStockModel toModel(PopInStock entity) { return createModelWithId(entity.getId(),entity); } //重写此方法则使用此创建Model对象,默认使用无参构造函数创建 @Override protected PopInStockModel instantiateModel(PopInStock entity) { return new PopInStockModel(entity); } }
Model: 一个对象实体的映射,会根据该映射实体创建连接,可以自定义需要暴露的属性
@Data @Relation(value = "pop",collectionRelation = "pops") //重命名生成的json字段名 public class PopInStockModel extends RepresentationModel<PopInStockModel> { private String code; private String name; //提供一个构造函数,设置spring调用此构造函数创建model对象,可以按自己需要进行赋值 public PopInStockModel(PopInStock popInStock) { this.code = popInStock.getCode(); this.name = popInStock.getName(); } }
json
-----请求: https://localhost:8443/popInStocks/list ---------------------- { "_embedded": { "pops": [{ "code": "R0B0505258", "name": "A", "_links": { "self": { "href": "https://localhost:8443/popInStocks/3" } } }, { "code": "R0B0505262", "name": "B", "_links": { "self": { "href": "https://localhost:8443/popInStocks/2" } } }] }, "_links": { "recents": { "href": "https://localhost:8443/popInStocks/list" } } } -----请求: https://localhost:8443/popInStocks/1 ---------------------- { "id": 1, "task": 934924, "code": "R0B0506432", "name": "A", "version": null, "brand": "A", "comment": null, "count": 1.0, "_links": { "popInStock": { "href": "https://localhost:8443/popInStocks/1" } } }
Spring Data REST
如果使用Spring Data那么通过引入Spring Data REST可以自动实现RESTful API
Maven依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency>
1. 调整资源链接和命名
通常只要引入依赖,则已经自动实现了实体的RESTful API
- 如: PopInStock.java实体, 请求:
https://localhost:8443/popInStocks
即可- 但是因为可能已经创建了此请求的Controller,此时需要重新定义RESTful API对应的请求连接
配置文件中配置根路径:
application.yml
spring: data: rest: base-path: /api
@RestResource
:Entity
中配置请求路径(path)和生成的json字段名(relation name)也可在
Repository
的方法上使用如:@RestResource(exported = false,...)
//设置不公开此方法@Data @Entity @Table(name = "POP_IN_STOCK") @RestResource(rel = "pops",path = "popInStocks") //配置名称和路径 public class PopInStock { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(name = "TASKID") private Integer task; //... }
@RepositoryRestController
: 作用与Controller
类上,为Controller
增加根路径/api
RepositoryRestResource
: 作用与Repository
上,可以设置是否公开此Repository
如:
@RepositoryRestResource(exported = false)
//设置不公开此RepositorySpring Data REST
自动生成的API只有CURD,如果想增加额外的API,则需要自己创建Controller使用@RepositoryRestController
注解(使用统一根路径),并且注册bean到Spring Data REST
中(自动生成的API会增加此自定义的Controller的导航)@Configuration public class HypermediaConfiguration{ //为PopInStock对象添加额外链接(自定义的处理/recent请求的Controller注册到Spring Data REST中去) //使Spring Data REST自动生成的API中可以导航到自定义Controller的连接 //@Bean //对EntityModel<PopInStock>对象类型添加额外连接recents public RepresentationModelProcessor<EntityModel<PopInStock>> popInStockProcessor(EntityLinks links){ return new RepresentationModelProcessor<EntityModel<PopInStock>>() { @Override public EntityModel<PopInStock> process(EntityModel<PopInStock> model) { model.add(links.linkFor(PopInStock.class).slash("recent").withRel("recents")); return model; } }; } @Bean //对CollectionModel<EntityModel<PopInStock>>对象添加额外连接 public RepresentationModelProcessor<CollectionModel<EntityModel<PopInStock>>> popInStocksProcessor(EntityLinks links){ return new RepresentationModelProcessor<CollectionModel<EntityModel<PopInStock>>>() { @Override public CollectionModel<EntityModel<PopInStock>> process(CollectionModel<EntityModel<PopInStock>> model) { model.add(links.linkFor(PopInStock.class).slash("recent").withRel("recents")); return model; } }; } }
2. 完整实例: 自定义Controller并添加到Spring Data REST自动生成的API中
Controller
注意: 因为
/recent
查询到的集合使用上例的Assembler
自动为集合内元素生成连接,因此上例PopController
注解修改为@RepositoryRestController
生成的连接才会自动添加跟路径/api
@RepositoryRestController public class PopDataRestController { @Autowired private JpaPopInStockDataRestRepository popRepository; private EntityLinks entityLinks; @Autowired public void setEntityLinks(EntityLinks entityLinks) { this.entityLinks = entityLinks; } //注意: /popInStocks前缀必须 @RequestMapping(method = RequestMethod.GET,path = "/popInStocks/recent",produces = "application/hal+json") //设置请求和响应类型 public ResponseEntity<CollectionModel<PopInStockModel>> recents(){ PageRequest pageRequest = PageRequest.of(0,5); List<PopInStock> list = popRepository.findPopInStocksByOrderByTaskDesc(pageRequest); CollectionModel<PopInStockModel> collections = new PopInStockAssembler().toCollectionModel(list); collections.add( entityLinks.linkFor(PopInStock.class) .slash("/recent") .withRel("recents")); return new ResponseEntity<>(collections, HttpStatus.OK); } }
Repository
//@RepositoryRestResource(path="popInStocks",collectionResourceRel="pops",itemResourceRel="pop") //@RepositoryRestResource(exported = false) 设置不公开此Repository public interface JpaPopInStockDataRestRepository extends CrudRepository<PopInStock,Integer> { //@RestResource(path="recent",rel="recents") //可在方法上设置 List<PopInStock> findPopInStocksByOrderByTaskDesc(Pageable pageable); @RestResource(exported = false)//设置不公开此方法 PopInStock findById(int id); }
Configuration: 将自定义的Controller注册到Spring Data REST自动生成的API中
@Configuration //@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) public class HypermediaConfiguration{ //为PopInStock对象添加额外链接(自定义的处理/recent请求的Controller注册到Spring Data REST中去) //使Spring Data REST自动生成的API中可以导航到自定义Controller的连接 @Bean public RepresentationModelProcessor<EntityModel<PopInStock>> popInStockProcessor(EntityLinks links){ return new RepresentationModelProcessor<EntityModel<PopInStock>>() { @Override public EntityModel<PopInStock> process(EntityModel<PopInStock> model) { model.add(links.linkFor(PopInStock.class).slash("recent").withRel("recents")); return model; } }; } }
application.yml
: 增加统一根路径spring: data: rest: base-path: /api
pom.xml
: Spring Data REST 所需额外Maven依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency>