1.背景
在后端编程中,通常关心接口的以下4个部分:接口地址(url)、接口请求方式(method)、请求数据(parameter)、响应数据(response)
本项目对后端接口的请求和响应数据进行规范,实现统一接口规范
创建SpringBoot项目,导入web依赖包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.10.RELEASE</version>
<relativePath/>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.poiuy</groupId>
<version>1.0-SNAPSHOT</version>
<artifactId>springboot-handler-controller</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.13</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
</project>
2.参数验证
接口一般需要对请求参数进行校验,用来过滤非法请求
常见以及最简单的校验方式就是在业务层进行参数校验,如下所示
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService){
this.userService = userService;
}
@PutMapping
public R save(@RequestBody User user){
if(user.getUserId() == null){
return R.error("用户id不能为空");
}
if(StringUtils.isBlank(user.getUsername())){
return R.error("用户账号不能为空");
}
if(user.getUsername().length() > 11 || user.getUsername().length() < 6){
return R.error("账号长度必须是6-11个字符");
}
if (StringUtils.isBlank(user.getSecret())){
return R.error("用户密码不能为空");
}
if(user.getSecret().length() > 11 || user.getSecret().length() < 6) {
return R.error("密码长度必须是6-16个字符");
}
if(StringUtils.isBlank(user.getEmail())){
return R.error("用户邮箱不能为空");
}
userService.save(user);
return R.ok();
}
}
使用PostMan测试请求示例如下
在业务层进行参数校验可以达到过滤数据的效果,但是问题也比较明显
- 相同的参数在不同的接口中都要验证一遍,造成代码冗余
- 如果一个验证规则发生变化,就要去所有使用到的业务层中进行修改
- 从上面的代码也可以看到,业务层的主要逻辑还没开始,就包含了大量的参数校验代码
此时我们使用SpringValidator和HibernateValidator这两个Validator来进行参数校验
首先在入参的字段上增加校验规则
public class User {
@NotNull(message = "用户id不能为空")
private Long userId;
@NotNull(message = "用户名不能为空")
@Length(min = 6,max = 12)
private String username;
@NotNull(message = "用户密码不能为空")
@Length(min = 6,max = 12)
private String secret;
@NotNull(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
然后在接口的请求参数上增加@Valid注解,并添加BindingResult参数来获取校验结果
此时如果参数校验不通过,可以通过BindingResul来获取具体原因,但是每个接口都需要添加BindingResult参数,然后再提取错误信息返回给前端,有点麻烦,重复代码也很多
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService){
this.userService = userService;
}
@PutMapping
public R save(@RequestBody @Valid User user, BindingResult result){
for (ObjectError error : result.getAllErrors()) {
return R.error(error.getDefaultMessage());
}
userService.save(user);
return R.ok();
}
}
3.异常处理
介于以上的问题,可以尝试将BindingResult参数去掉,测试一下会出现什么情况
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService){
this.userService = userService;
}
@PutMapping
public R save(@RequestBody @Valid User user){
userService.save(user);
return R.ok();
}
}
可以看到接口抛出了异常,并且接口不会继续执行
这是我们可以使用全局异常处理来解决这一问题
添加异常处理类,并使用@ControllerAdvice或@RestControllerAdvice注解,然后在类方法上添加@ExceptionHandler并指定异常类,那么当接口抛出对应的异常时就会使用全局异常处理类方法中进行处理
@RestControllerAdvice
public class PoiExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 处理自定义异常
*/
@ExceptionHandler(PoiException.class)
public R handlePoiException(PoiException e){
R r = new R();
r.put("code", e.getCode());
r.put("msg", e.getMessage());
return r;
}
@ExceptionHandler(NoHandlerFoundException.class)
public R handlerNoFoundException(Exception e) {
logger.error(e.getMessage(), e);
return R.error(404, "路径不存在,请检查路径是否正确");
}
@ExceptionHandler(DuplicateKeyException.class)
public R handleDuplicateKeyException(DuplicateKeyException e){
logger.error(e.getMessage(), e);
return R.error("数据库中已存在该记录");
}
// @ExceptionHandler(AuthorizationException.class)
// public R handleAuthorizationException(AuthorizationException e){
// logger.error(e.getMessage(), e);
// return R.error("没有权限,请联系管理员授权");
// }
@ExceptionHandler(MethodArgumentNotValidException.class)
public R handleNotValidException(MethodArgumentNotValidException e){
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return R.error(objectError.getDefaultMessage());
}
@ExceptionHandler(Exception.class)
public R handleException(Exception e){
logger.error(e.getMessage(), e);
return R.error();
}
}
4.自定义异常
项目中不可能只抛出一种异常,对每一种异常都增加一个方法来处理也不实际
添加自定义异常,在项目中出现异常的情况下都包装为自定义异常,然后在全局异常处理中进行处理
public class PoiException extends RuntimeException {
private static final long serialVersionUID = 1L;
private String msg;
private int code = 500;
public PoiException(String msg) {
super(msg);
this.msg = msg;
}
public PoiException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public PoiException(String msg, int code) {
super(msg);
this.msg = msg;
this.code = code;
}
public PoiException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
5.返回结果
统一数据响应,自定义一个响应体类,无论后台是正常运行还是发生异常,都返回该类给前端
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R() {
put("code", 0);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
最后返回数据格式: