05 SpringBoot初体验:Restful风格接口开发

05 SpringBoot初体验:Restful风格接口开发

背景

Web前后端开发中,后端需要为前端提供接口。那么我们如何来提供一套相对优雅的接口设计实践?目前Http比较流行的使用Restful风格。且看一下如何在项目中比较优雅的落地Restful接口。

目标

落地一份相较优雅通用的Restful接口实践。

准备知识

Restful风格

关键点:数据和操作解耦,接口设计简单理解为找到资源+操作资源。大家有这样的思路就能更好的理解和实现Restful风格接口。

  1. Restful风格将数据看待为资源,那么接口就是操作资源的入口/方法。
  2. 结合Http协议,以HttpMethod作为操作动作,URI以复数名词作为资源访问路径:
    1. Get:单个获取、批量获取、条件列取 -> GET /api/users/{id}
    2. POST:新增单个 -> POST /api/users
    3. PUT:全量更新单个 -> PUT /api/users/{id}
    4. PATCH:增量更新单个 (不常用,一般开发习惯使用PUT支持增量更新)
    5. DELETE:删除单个 -> DELETE /api/users/{id}

SpringWeb

  1. 注解:@RestController、@PathVariable、@RequestParam、@RequestBody~~、@RequestHeader、@RequestAttribute、@ResponseEntity~~
  2. HttpMethod:GET、POST、PUT、DELETE、PATCH…

实践

这里我们简单的以用户服务接口示例。

标准单资源Restful接口

URI表示为复数名词/users。那么对于单用户的CRUD操作如下:

请求描述URI示例备注
创建一个用户POST /usersPOST /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传参用户idDELETE类似GET语义上不使用@RequestBody
分页列取用户GET``/usersGET /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接口

Google API设计指南

非标准资源接口设计思路仍是资源+操作的思路,但此时操作要结合语义使用HttpMethod和自定义方法。对于HttpMethod整体上如果是查询的语义,我们仍尽量使用GET、否则一般使用POST,资源和自定义方法用一个动词表示对资源的操作)间使用冒号(:)分割。

批量接口

非标准方法的第1个场景:批量操作。比如为提升性能,提供批量插入、更新、删除、获取的接口。

请求描述URI示例备注
批量创建用户POST /users:mpostPOST /users:mpost
[{"name":"test"}]
自定义方法:mpost,@RequestBody传参为用户信息数组。
批量接口要结合性能考虑设置批量大小
批量查询用户GET /users:mgetGET /users:mget?ids=1,2,3自定义方法:mget,@RequestParam传参用户id数组
注意@RequestParam传参最多2048字节
批量修改用户PUT /users:mputPUT /users:mput
[{"id":1}]
自定义方法:mput@RequestBody传参为用户信息数组
批量删除用户DELETE /users:mdeleteDELETE /users:mdelete?ids=1,2,3自定义方法:mdelete@RequestParam传参为用户id数组
批量清空用户DELETE``/users:clearDELETE /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 /usersGET /users?name=小明&pageNo=1&pageSize=10简单条件查询,传参条件User,支持全词/前缀匹配
复杂查询用户GET /users:serarchGET /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}:movePUT /orgs/1/users/1:move
{"orgId":"2"}
存在父子资源关系时的移动到另一个父级下
非简单属性修改(停用用户)PUT users/{id}:disablePUT /users/1:disable标准的资源方法不合适表达语义, 如重启机器、发送邮件 (通常体现出状态、流程的变化)
用户行为(向用户发通知)PUT /users/{id}:notifyPUT /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() + ",你中奖了,喜获开心大礼包一份!");
    }
}

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

theskyzero

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值