往期热门文章:
1、项目终于用上了Spring状态机,非常优雅!
(相关资料图)
2、自从用了这款 IDEA 神器,领导都夸我代码写得好!
3、MyBatis的二级缓存,慎用!
4、8种专坑同事的 SQL 写法,性能降低100倍,不来看看?
5、最近火起的 Bean Searcher 与 MyBatis Plus 到底有啥区别?
一、前言
一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。虽然说后端接口的编写并没有统一规范要求,而且如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但其中最重要的关键点就是看是否规范。
二、环境说明
因为讲解的重点是后端接口,所以需要导入一个 spring-boot-starter-web 包,而 lombok 作用是简化类,前端显示则使用了 knife4j,具体使用在 Spring Boot 整合 knife4j 实现 API 文档已写明。另外从 springboot-2.3 开始,校验包被独立成了一个 starter 组件,所以需要引入如下依赖:
三、参数校验
3.1介绍
一个接口一般对参数(请求数据)都会进行安全校验,参数校验的重要性自然不必多说,那么如何对参数进行校验就有讲究了。一般来说有三种常见的校验方式,我们使用了最简洁的第三种方法:
业务层校验
Validator + BindResult 校验
Validator + 自动抛出异常
业务层校验无需多说,即手动在 Java 的 Service 层进行数据校验判断。不过这样太繁琐了,光校验代码就会有很多。
而使用 Validator+ BindingResult 已经是非常方便实用的参数校验方式了,在实际开发中也有很多项目就是这么做的,不过这样还是不太方便,因为你每写一个接口都要添加一个 BindingResult 参数,然后再提取错误信息返回给前端(简单看一下)。
@PostMapping("/addUser")public String addUser(@RequestBody @Validated User user, BindingResult bindingResult) { // 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里 List3.2 Validator + 自动抛出异常(使用)
内置参数校验如下:
首先,Validator可以非常方便的制定校验规则,并自动帮你完成校验。在入参里需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:
@Datapublic class User { @NotNull(message = "用户id不能为空") private Long id; @NotNull(message = "用户账号不能为空") @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符") private String account; @NotNull(message = "用户密码不能为空") @Size(min = 6, max = 11, message = "密码长度必须是6-16个字符") private String password; @NotNull(message = "用户邮箱不能为空") @Email(message = "邮箱格式不正确") private String email;}校验规则和错误提示信息配置完毕后,接下来只需要在接口仅需要在校验的参数上加上 @Valid 注解(去掉 BindingResult 后会自动引发异常,异常发生了自然而然就不会执行业务逻辑):
@RestController@RequestMapping("user")public class ValidationController { @Autowired private ValidationService validationService; @PostMapping("/addUser") public String addUser(@RequestBody @Validated User user) { return validationService.addUser(user); }}现在我们进行测试,打开 knife4j 文档地址,当输入的请求数据为空时,Validator 会将所有的报错信息全部进行返回,所以需要与全局异常处理一起使用。
// 使用form data方式调用接口,校验异常抛出 BindException// 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException// 单个参数校验异常抛出ConstraintViolationException// 处理 json 请求体调用接口校验失败抛出的异常@ExceptionHandler(MethodArgumentNotValidException.class)public ResultVO3.3 分组校验和递归校验
分组校验有三个步骤:
定义一个分组类(或接口)
在校验注解上添加 groups 属性指定分组
Controller 方法的 @Validated 注解添加分组类
public interface Update extends Default{}@Datapublic class User { @NotNull(message = "用户id不能为空",groups = Update.class) private Long id; ......}@PostMapping("update")public String update(@Validated({Update.class}) User user) { return "success";}如果Update不继承Default,@Validated({Update.class})就只会校验属于Update.class分组的参数字段;如果继承了,会校验了其他默认属于Default.class分组的字段。
对于递归校验(比如类中类),只要在相应属性类上增加@Valid注解即可实现(对于集合同样适用)
3.4 自定义校验
Spring Validation 允许用户自定义校验,实现很简单,分两步:
自定义校验注解
编写校验者类
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })@Retention(RUNTIME)@Documented@Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑public @interface HaveNoBlank { // 校验出错时默认返回的消息 String message() default "字符串中不能含有空格"; Class>[] groups() default { }; Class extends Payload>[] payload() default { }; /** * 同一个元素上指定多个该注解时使用 */ @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented public @interface List { NotBlank[] value(); }}public class HaveNoBlankValidator implements ConstraintValidator四、全局异常处理
参数校验失败会自动引发异常,我们当然不可能再去手动捕捉异常进行处理。但我们又不想手动捕捉这个异常,又要对这个异常进行处理,那正好使用SpringBoot全局异常处理来达到一劳永逸的效果!
4.1 基本使用
首先,我们需要新建一个类,在这个类上加上 @ControllerAdvice 或 @RestControllerAdvice 注解,这个类就配置成全局处理类了。
这个根据你的 Controller 层用的是 @Controller 还是 @RestController 来决定。
然后在类中新建方法,在方法上加上 @ExceptionHandler 注解并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理!我们现在就来演示一下对参数校验失败抛出的 MethodArgumentNotValidException 全局处理:
package com.csdn.demo1.global;import org.springframework.validation.ObjectError;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice@ResponseBodypublic class ExceptionControllerAdvice { @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { // 从异常对象中拿到ObjectError对象 ObjectError objectError = e.getBindingResult().getAllErrors().get(0); // 然后提取错误提示信息进行返回 return objectError.getDefaultMessage(); } /** * 系统异常 预期以外异常 */ @ExceptionHandler(Exception.class) @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ResultVO> handleUnexpectedServer(Exception ex) { log.error("系统异常:", ex); // GlobalMsgEnum.ERROR是我自己定义的枚举类 return new ResultVO<>(GlobalMsgEnum.ERROR); } /** * 所以异常的拦截 */ @ExceptionHandler(Throwable.class) @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ResultVO> exception(Throwable ex) { log.error("系统异常:", ex); return new ResultVO<>(GlobalMsgEnum.ERROR); }}我们再次进行测试,这次返回的就是我们制定的错误提示信息!我们通过全局异常处理优雅的实现了我们想要的功能!
以后我们再想写接口参数校验,就只需要在入参的成员变量上加上 Validator 校验规则注解,然后在参数上加上 @Valid 注解即可完成校验,校验失败会自动返回错误提示信息,无需任何其他代码!
4.2 自定义异常
在很多情况下,我们需要手动抛出异常,比如在业务层当有些条件并不符合业务逻辑,而使用自定义异常有诸多优点:
自定义异常可以携带更多的信息,不像这样只能携带一个字符串。
项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。
自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
我们现在就来开始写一个自定义异常:
package com.csdn.demo1.global;import lombok.Getter;@Getter //只要getter方法,无需setterpublic class APIException extends RuntimeException { private int code; private String msg; public APIException() { this(1001, "接口错误"); } public APIException(String msg) { this(1001, msg); } public APIException(int code, String msg) { super(msg); this.code = code; this.msg = msg; }}然后在刚才的全局异常类中加入如下:
//自定义的全局异常@ExceptionHandler(APIException.class)public String APIExceptionHandler(APIException e) { return e.getMsg();}这样就对异常的处理就比较规范了。当然还可以添加对 Exception 的处理,这样无论发生什么异常我们都能屏蔽掉然后响应数据给前端,不过建议最后项目上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了方便调试还是不要这样做。
另外,当我们抛出自定义异常的时候全局异常处理只响应了异常中的错误信息 msg 给前端,并没有将错误代码 code 返回。这还需要配合数据统一响应。
如果在多模块使用,全局异常等公共功能抽象成子模块,则在需要的子模块中需要将该模块包扫描加入,@SpringBootApplication(scanBasePackages = {"com.xxx"})。
五、数据统一响应
统一数据响应是我们自己自定义一个响应体类,无论后台是运行正常还是发生异常,响应给前端的数据格式是不变的!这里我包括了响应信息代码 code 和响应信息说明 msg,首先可以设置一个枚举规范响应体中的响应码和响应信息。
@Getterpublic enum ResultCode { SUCCESS(1000, "操作成功"), FAILED(1001, "响应失败"), VALIDATE_FAILED(1002, "参数校验失败"), ERROR(5000, "未知错误"); private int code; private String msg; ResultCode(int code, String msg) { this.code = code; this.msg = msg; }}自定义响应体:
package com.csdn.demo1.global;import lombok.Getter;@Getterpublic class ResultVO最后需要修改全局异常处理类的返回类型:
@RestControllerAdvicepublic class ExceptionControllerAdvice { @ExceptionHandler(APIException.class) public ResultVO最后,在 controller 层进行接口信息数据的返回:
@GetMapping("/getUser")public ResultVO经过测试,这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!
还有一种全局返回类如下:
@Data@AllArgsConstructor@NoArgsConstructorpublic class Msg { //状态码 private int code; //提示信息 private String msg; //用户返回给浏览器的数据 private Map六、全局处理响应数据(可选择)
接口返回统一响应体 + 异常也返回统一响应体,其实这样已经很好了,但还是有可以优化的地方。要知道一个项目下来定义的接口搞个几百个太正常不过了,要是每一个接口返回数据时都要用响应体来包装一下好像有点麻烦,有没有办法省去这个包装过程呢?
当然是有的,还是要用到全局处理。但是为了扩展性,就是允许绕过数据统一响应(这样就可以提供多方使用),我们可以自定义注解,利用注解来选择是否进行全局响应包装。
首先,创建自定义注解,作用相当于全局处理类开关:
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD}) // 表明该注解只能放在方法上public @interface NotResponseBody {}其次,创建一个类并加上注解使其成为全局处理类。然后继承 ResponseBodyAdvice 接口重写其中的方法,即可对我们的 controller 进行增强操作,具体看代码和注释:
package com.csdn.demo1.global;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.core.MethodParameter;import org.springframework.http.MediaType;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.web.bind.annotation.RestControllerAdvice;import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;@RestControllerAdvice(basePackages = {"com.scdn.demo1.controller"}) // 注意哦,这里要加上需要扫描的包public class ResponseControllerAdvice implements ResponseBodyAdvice