05 SpringBoot初体验:Restful风格接口开发
文章目录
背景
Web
前后端开发中,后端需要为前端提供接口。那么我们如何来提供一套相对优雅的接口设计实践?目前Http
比较流行的使用Restful
风格。且看一下如何在项目中比较优雅的落地Restful
接口。
目标
落地一份相较优雅通用的Restful
接口实践。
准备知识
Restful风格
关键点:数据和操作解耦,接口设计简单理解为找到资源+操作资源。大家有这样的思路就能更好的理解和实现
Restful
风格接口。
Restful
风格将数据看待为资源,那么接口就是操作资源的入口/方法。- 结合
Http
协议,以HttpMethod
作为操作动作,URI
以复数名词作为资源访问路径:Get
:单个获取、批量获取、条件列取 ->GET /api/users/{id}
POST
:新增单个 ->POST /api/users
PUT
:全量更新单个 ->PUT /api/users/{id}
PATCH
:增量更新单个 (不常用,一般开发习惯使用PUT
支持增量更新)DELETE
:删除单个 ->DELETE /api/users/{id}
SpringWeb
- 注解:@RestController、@PathVariable、@RequestParam、@RequestBody~~、@RequestHeader、@RequestAttribute、@ResponseEntity~~
- HttpMethod:GET、POST、PUT、DELETE、PATCH…
实践
这里我们简单的以用户服务接口示例。
标准单资源Restful
接口
URI
表示为复数名词/users。那么对于单用户的CRUD
操作如下:
请求描述 | URI | 示例 | 备注 |
---|---|---|---|
创建一个用户 | POST /users | POST /users {"name":"test"} | 通过@RequestBody 传参待创建的用户信息 |
查询某个用户 | GET /users/{id} | GET /user/1 | 通过@Pathvariable 传参用户id ,建议GET 场景不要使用@RequestBody |
修改某个用户 | PUT /users/{id} | PUT /users/1 {"name":"test"} | 通过@Pathvariable 传参用户id ,@RequestBody 传参待修改的用户信息 |
删除某个用户 | DELETE /users/{id} | DELETE /user/1 | 通过@Pathvariable 传参用户id ,DELETE 类似GET 语义上不使用@RequestBody |
分页列取用户 | GET``/users | GET /users?name=test&pageNo=1&pageSize=10 | 比较通用的列取查询,比如按用户某个属性分页列取 |
package com.gitee.theskyone.bird.web.rest;
@RestController
@RequestMapping("/users")
public class UserRestController {
ConcurrentHashMap<Long, User> userRepository = new ConcurrentHashMap<>();
/**
* 新增一个用户
*
* @param user user
* @return
*/
@PostMapping
public Response<User> createUser(@RequestBody User user) {
user.setId(RandomUtils.nextLong(0, Integer.MAX_VALUE));
userRepository.put(user.getId(), user);
return Response.success(user);
}
/**
* 按用户id查询用户
*
* @param id 用户id
* @return
*/
@GetMapping("/{id}")
public Response<User> findUser(@PathVariable Long id) {
return Response.success(userRepository.get(id));
}
/**
* 更新一个用户
*
* @param id 用户id
* @param user user
* @return
*/
@PutMapping("/{id}")
public Response<User> modifyUser(@PathVariable Long id, @RequestBody User user) {
User old = userRepository.get(id);
user.setId(id);
userRepository.put(id, user);
return Response.success(old);
}
/**
* 删除一个用户
*
* @param id
* @return
*/
@DeleteMapping("/{id}")
public Response<User> removeUser(@PathVariable Long id) {
return Response.success(userRepository.remove(id));
}
}
我们可以看到,Restful
对单资源的操作是比较友好的,容易在项目中达成共识。但往往随着需求迭代,我们也会需要更为复杂的接口,如常用的批量操作或者更复杂繁多的查询视图,这个时候还能优雅的使用Restful
吗?
这类我们称之为"非标准"方法,然后这里我们使用Google API
的设计来落地非标准方法的实践。
非标准资源Restful接口
非标准资源接口设计思路仍是资源+操作的思路,但此时操作要结合语义使用HttpMethod
和自定义方法。对于HttpMethod
整体上如果是查询的语义,我们仍尽量使用GET
、否则一般使用POST
,资源和自定义方法(用一个动词表示对资源的操作)间使用冒号(:)分割。
批量接口
非标准方法的第1个场景:批量操作。比如为提升性能,提供批量插入、更新、删除、获取的接口。
请求描述 | URI | 示例 | 备注 |
---|---|---|---|
批量创建用户 | POST /users:mpost | POST /users:mpost [{"name":"test"}] | 自定义方法:mpost ,@RequestBody 传参为用户信息数组。批量接口要结合性能考虑设置批量大小 |
批量查询用户 | GET /users:mget | GET /users:mget?ids=1,2,3 | 自定义方法:mget ,@RequestParam 传参用户id数组注意 @RequestParam 传参最多2048字节 |
批量修改用户 | PUT /users:mput | PUT /users:mput [{"id":1}] | 自定义方法:mput ,@RequestBody 传参为用户信息数组 |
批量删除用户 | DELETE /users:mdelete | DELETE /users:mdelete?ids=1,2,3 | 自定义方法:mdelete ,@RequestParam 传参为用户id数组 |
批量清空用户 | DELETE``/users:clear | DELETE /users:clear | 自定义批量的一种case |
package com.gitee.theskyone.bird.web.rest;
@RestController
@RequestMapping("/users")
public class UserRestController {
ConcurrentHashMap<Long, User> userRepository = new ConcurrentHashMap<>();
/**
* 批量创建用户
*
* @param users users
* @return
*/
@PostMapping(":mpost")
public Response<List<User>> multiCreateUser(List<User> users) {
users.forEach(user -> user.setId(RandomUtils.nextLong(0, Integer.MAX_VALUE)));
List<User> userList = users.stream()
.map(user -> userRepository.put(user.getId(), user))
.collect(Collectors.toList());
return Response.success(userList);
}
/**
* 批量查询用户
*
* @param ids 用户ids
* @return
*/
@GetMapping(":mget")
public Response<List<User>> multiFindUser(@RequestParam List<Long> ids) {
List<User> users = ids.stream()
.map(id -> userRepository.get(id))
.filter(Objects::nonNull)
.collect(Collectors.toList());
return Response.success(users);
}
/**
* 批量更新用户
*
* @param users
* @return
*/
@PutMapping(":mput")
public Response<List<User>> multiModifyUser(@RequestBody List<User> users) {
List<User> userList = users.stream()
.map(user -> userRepository.put(user.getId(), user))
.collect(Collectors.toList());
return Response.success(userList);
}
@DeleteMapping(":mdelete")
public Response<List<User>> multiRemoveUser(@RequestParam List<Long> ids) {
List<User> users = ids.stream()
.map(id -> userRepository.remove(id))
.collect(Collectors.toList());
return Response.success(users);
}
}
复杂查询接口
非标准方法的第2个场景:复杂的查询。比如提供复杂的条件查询或者提供多个查询视图。条件查询我们直接支持分页。
请求描述 | URI | 示例 | 备注 |
---|---|---|---|
简单条件分页查询 | GET /users | GET /users?name=小明&pageNo=1&pageSize=10 | 简单条件查询,传参条件User,支持全词/前缀匹配 |
复杂查询用户 | GET /users:serarch | GET /users:serarch?name=小明,小芳 | 自定义复杂的查询条件对象。更更复杂的要引入一套DSL了… |
多查询视图 | GET /users/{id} | GET /users/1?view=noPassword | 类似按id查询单资源,id查询使用@Pathvariable ,这里view 只用来区分接口,可以使用@RequestParam 或者@RequestHeader 替代 |
package com.gitee.theskyone.bird.web.rest;
@RestController
@RequestMapping("/users")
public class UserRestController {
ConcurrentHashMap<Long, User> userRepository = new ConcurrentHashMap<>();
@GetMapping
public Response<Page<User>> listUser(User condition, Page<?> page) {
List<User> users = new ArrayList<>();
if (Objects.nonNull(condition.getId())) {
User find = userRepository.get(condition.getId());
if (Objects.nonNull(find)) {
users.add(find);
}
}
if (Objects.nonNull(condition.getName())) {
userRepository.forEach((id, user) -> {
if (user.getName().equals(condition.getName())) {
users.add(user);
}
});
}
return Response.success(Page.of(page, users));
}
@GetMapping(":search")
public Response<Page<User>> searchUser(User condition, Page<?> page) {
List<User> users = new ArrayList<>();
if (Objects.nonNull(condition.getId())) {
User find = userRepository.get(condition.getId());
if (Objects.nonNull(find)) {
users.add(find);
}
}
if (Objects.nonNull(condition.getName())) {
userRepository.forEach((id, user) -> {
if (user.getName().toLowerCase().contains(condition.getName().toLowerCase())) {
users.add(user);
}
});
}
return Response.success(Page.of(page, users));
}
@GetMapping(value = "/{id}", params = "view=noPassword")
public Response<User> findUserNoPassword(@PathVariable Long id) {
return Response.success(userRepository.get(id));
}
}
状态变化类接口
比如开关、启停,发送邮件等类似操作通常会产生状态的变化,天然的就是一个操作。不过结合上面的实践,我们仍然能将这类操作定义成资源+自定义操作形式的Restful
接口。
请求描述 | URI | 示例 | 备注 |
---|---|---|---|
移动用户(组织下) | PUT /orgs/1/users/{id}:move | PUT /orgs/1/users/1:move {"orgId":"2"} | 存在父子资源关系时的移动到另一个父级下 |
非简单属性修改(停用用户) | PUT users/{id}:disable | PUT /users/1:disable | 标准的资源方法不合适表达语义, 如重启机器、发送邮件 (通常体现出状态、流程的变化) |
用户行为(向用户发通知) | PUT /users/{id}:notify | PUT /users/1:notify | 标准的资源方法不合适表达语义, 如重启机器、发送邮件 (通常体现出状态、流程的变化) |
package com.gitee.theskyone.bird.web.rest;
@RestController
@RequestMapping("/users")
public class UserRestController {
ConcurrentHashMap<Long, User> userRepository = new ConcurrentHashMap<>();
@GetMapping("/{id}:disable")
public Response<User> disableUser(@PathVariable Long id) {
User user = userRepository.get(id);
if (Objects.isNull(user)) {
throw new BizException("用户不存在,id=" + id, BizErrorEnum.A0000);
}
user.setEnable(false);
userRepository.put(id, user);
return Response.success(user);
}
@GetMapping("/{id}:notify")
public Response<String> notifyUser(@PathVariable Long id) {
User user = userRepository.get(id);
if (Objects.isNull(user)) {
throw new BizException("用户不存在,id=" + id, BizErrorEnum.A0000);
}
return Response.success("恭喜你:" + user.getName() + ",你中奖了,喜获开心大礼包一份!");
}
}