SpringBoot 中接口规范

本文主要记录,我在学习使用 SpringBoot 开发接口的一些技巧

一、接口请求参数方式

目前主流的请求参数有两种:form-urlencoded 和 application/json(大佬推荐),两种不同的方式 JAVA 端读取参数的方式略有不同

1. form-urlencoded

Content-Type:application/x-www-form-urlencoded

参数类型,主要是通过:键值对传递,例如:username: 'zZ爱吃菜'

@RequestParam("key")
public void register(@RequestParam String username) {
  // 推荐使用日志模式输入内容
  log.info("username={}", username);
}
// 接受参数的方式
public void register(@RequestParam String username) {
  // 推荐使用日志模式输入内容
  log.info("username={}", username);
}
// 参数重命名, 同上
public void register(@RequestParam("username") String userName) {
  // 推荐使用日志模式输入内容
  log.info("username={}", userName);
}
// 直接将参数复制给一个对象,前提参数名与对象属性名对应
public void register(User user) {
  // 推荐使用日志模式输入内容
  log.info("username={}", user.getUsername());
}

2. application/json

Content-Type:application/json

参数形式: json 形式

{
	"username": "zZ 爱吃菜",
	"password": "123123"
}

java 接收,需要增加 @RequestBody 注解,来接收

// 如果是 Post 方式 Content-Type: application/json,则使用@RequestBody 方式接收参数
public void register(@RequestBody User user) {
  // 推荐使用日志模式输入内容
  log.info("username={}", user.toString());
}

二、接口返回

在制作接口通常都是放回 JSON 数据给前端

SpringBoot 有专门的注解返回 JSON

以前:@Controller + @ResponseBody

现在:@RestController 即可

1. 常规返回标准化接口数据

通常的标准化接口数据格式如下

// 正确返回值
{
    "status": 1,
    "msg": "***操作成功",
  	"data": {
      "key": "value",
      ...
    }
}
// 专属错误返回值
{
    "status": 2,
    "msg": "用户未登录,请先登录"
}
// 错误返回值(通用)
{
    "status": -1,
    "msg": "系统错误"
}

2.实现标准版接口返回数据

在项目下,创建 vo 包(view object缩写),创建 ResponseVo.java 类

构建通用的 接口返回内容模块,另外需要跟 接口返回状态 status 对应的枚举匹配

package com.imooc.mall.vo;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.imooc.mall.enums.ResponseEnum;
import lombok.Data;

@Data
//@JsonSerialize
// 让 json 接口返回的 null 隐藏掉
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class ResponseVo<T> {

    private Integer status;

    private String msg;

    // data 数据不确定,所以使用 泛型
    private T data;

    public ResponseVo(Integer status, String msg) {
        this.status = status;
        this.msg = msg;
    }
    // 指定msg 成功返回结果
    public static <T> ResponseVo<T> success(String msg) {
        return new ResponseVo(ResponseEnum.SUCCESS.getCode(), msg);
    }
    // 默认成功返回结果
    public static <T> ResponseVo<T> success() {
        return new ResponseVo(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getDesc());
    }
    // 错误内容返回
    public static <T> ResponseVo<T> error(ResponseEnum responseEnum) {
        return new ResponseVo(responseEnum.getCode(), responseEnum.getDesc());
    }
}

接口状态枚举累 ResponseEnum

注意:枚举状态中的状态码,没必要跟我一致,我这里也是根据课程内容写的,具体开发阶段还是需要自己跟前端开发工程师去沟通。因为接口是给前端交互的,而不是满足自己的。

package com.imooc.mall.enums;

import lombok.Getter;

@Getter
public enum ResponseEnum {

    ERROR(-1, "服务端错误"),

    SUCCESS(0, "成功"),

    PASSWORD_ERROR(1, "密码错误"),

    USER_EXIST(2, "用户 已存在"),

    NEED_LOGIN(10, "用户未登录,请先登录"),
    ;

    Integer code;

    String desc;

    ResponseEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

最近简单在 Controller 中调用一下

// 实现接口返回 JSON 格式
@RestController
// 定义 Controller 请求路径
@RequestMapping("/user")
// 日志打印注解,开发阶段很需要
@Slf4j
public class UserController {
    @PostMapping(value = "/register")
    public ResponseVo register(@RequestBody User user) {
        // 推荐使用日志模式输入内容
        log.info("username={}", user.toString());
        //return ResponseVo.success();
        return ResponseVo.error(NEED_LOGIN);
    }
}

三、表单验证

一般接口都会接收参数,那么我们需要怎么校验没个参数是否接收到呢?

答:为每个接口增加一个表单类,用来控制每个参数是否为必须参数

以用户注册为例

1. 创建一个表单类

通常表单类,放在 form 包下: src/main/java/工程名/form/

注意:如下注解对应不同字段即不同参数的要求。

@NotBlank // 用户 String 判断空格 @NotEmpty // 用于集合 @NotNull // 参数不能为 null , 可以为空字符串

import lombok.Data;

import javax.validation.constraints.NotBlank;

@Data
public class UserForm {

    // @NotBlank 用户 String 判断空格
    // @NotEmpty // 用于集合
    // @NotNull
    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "用户密码不能为空")
    private String password;

    @NotBlank(message = "用户邮箱不能为空")
    private String email;
}

2. Controller 校验

重点:@Valid + BindingResult 实现对 UserForm 接口参数表单内容的校验

UserForm 来接收 接口发来的参数(@RequestBody 中的参数),@Valid 注解帮助我们获取到 UserForm 参数是否符合要求,通过 BindingResult 输出是否有错误

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private IUserService userService;

    @PostMapping(value = "/register")
    public ResponseVo register(@Valid @RequestBody UserForm userForm,
                               BindingResult bindingResult) {
        // 表单校验
        if (bindingResult.hasErrors()) {
          	log.error("注册提交的参数有误, {} {}",
                    bindingResult.getFieldError().getField(),
                    bindingResult.getFieldError().getDefaultMessage());
            return ResponseVo.error(PARAM_ERROR, bindingResult);
        }
        User user = new User();
        BeanUtils.copyProperties(userForm, user);
        return userService.register(user);
    }
}
方法 描述
bindingResult.getFieldError().getField() 获取不符合要求的字段
bindingResult.getFieldError().getDefaultMessage() 输入字段描述的错误信息, 对应 @NotBlank(message = "用户名不能为空")

3. 最后重载一下 ResponseVo 的 error 方法,

实现自动对 bindingResult 输出

public static <T> ResponseVo<T> error(ResponseEnum responseEnum, BindingResult bindingResult) {
        return new ResponseVo(responseEnum.getCode(), responseEnum.getDesc() + ":" + Objects.requireNonNull(bindingResult.getFieldError()).getField() + " " + bindingResult.getFieldError().getDefaultMessage());
    }

4.最终错误参数接口输出结果

{
    "status": 4,
    "msg": "邮箱已存在"
}

总结

不论接口参数多少,都可以统一在接口参数表单类里进行设置 判断条件(通过注解)并设置默认错误信息,然后即可以统一使用 @Valid + BindingResult + ResponseVo 输出错误结果,提高代码的简洁度和可读性,降低代码的耦合度。

四、运行时错误,统一输出 JSON

在请求结果执行的过程中如果返现 throw new RuntimeException(“****”); 不可控的错误时,该如何友好的让前端获取到呢?这就需要查看一下如真的有异常输出,前端获取的 json 格式是怎么样的。

1. 模拟一个运行时错误

加入我们在 Service 层增加一个抛出 RuntimeException 的方法,并被调用。

// 模拟一个异常操作,然后输出错误
private void testError() {
  throw new RuntimeException("意外错误");
}

@Override
public ResponseVo register(User user) {
  // 测试异常
  testError();
  /// 下面时注册逻辑;
}

接口返回的结果

{
    "timestamp": "2020-04-21T14:51:21.840+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "意外错误",
    "path": "/user/register"
}

看看吧,接口返回的内容 跟 我们与前端沟通的格式不同,前端同事会崩溃的~

2.通过设置异常处理类统一返回结果

创建一个 exception 包,并创建 RuntimeExceptionHandle 类 专门处理这些异常错误,并统一返回 json 格式

我的理解,这是通过拦截异常错误,获取错误内容,自己结合 ResponseVo 返回我们约定的 json 格式

@ControllerAdvice
public class RuntimeExceptionHandle {

    // 通过捕获异常,将运行时异常的输出与我们通用 API 返回 JSON 格式统一起来
    @ExceptionHandler(RuntimeException.class)
    @ResponseBody // 用于返回 json 数据
    //@ResponseStatus(HttpStatus.ACCEPTED) // 控制返回状态码
    public ResponseVo handle(RuntimeException e) {
        return ResponseVo.error(ResponseEnum.ERROR, e.getMessage());
    }

}

返回的 json 数据

{
    "status": -1,
    "msg": "服务端错误:意外错误"
}

在 Controller 中增加 HttpSession session 参数即可获得,session 对象,通过 session.setAttribute(key, value) session.getAttribute(key) 方式 设置session 和 读取。

session 和 cookie 的关系

  • session 存储在服务器,session.getId() 同 下面 cookie 对应
  • cookie 存储在客户端,内容为:JSESSIONID=0E245BE27CB3AE565CA68F004879ED9D
  • cookie 存储注意 跨域问题,即 domain, 比如 localhost 和 127.0.0.1 发同一个请求, JSESSIONID 是不同的

未完待续

  • session + cookie
  • 接口统一校验 等

最后说两句

其实已经开发了很多年了,不过都是其他技术栈(PHP+iOS+Flutter),这次在使用 SpringBoot 开发过程中,发现通过相对规范的方式开发系统,其实不会增加代码量和工作量,而且可以使项目更容易理解与维护。

Last Updated: 4/22/2020, 9:50:47 PM