SpringBoot使用validation实现接口参数统一校验、统一返回值、自定义注解

介绍

在日常得接口开发中,为了防止非法参数对业务造成影响,经常需要对接口得参数进行校验,例如注册用户得时候需要检验用户名密码是否为空,手机号邮箱格式是否正确。靠代码对接口参数一个个校验得话就太过于繁琐,代码可读性极差。

由此spring框架提供了一个参数校验得框架validator框架

validator框架就是为了解决开发人员在开发代码得时候仅用少于代码完成参数校验得工作,提升开发效率,validator框架是专门用来进行校验接口参数得,例如:

  1. 必填项校验
  2. email格式校验
  3. 手机号格式校验
  4. 参数值长度校验
  5. 正则表达式校验

接下来直接开始使用validator框架

 

1. spring boot 中集成参数校验

1.1. 引入相关依赖


  org.springframework.boot
  spring-boot-starter-validation

1.2. 定义参数实体类

package org.init.model.business;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {


    @NotBlank(message = "账号不能为空")
    @Length(min = 5,max = 16,message = "账号长度为5-16位")
    private String userName;
    @NotBlank(message = "密码不能为空")
    @Length(min = 4,max = 16,message = "密码长度为4-16位")
    private String passWord;
    @NotBlank(message = "账号不能为空")
    @Email(message = "邮箱格式出现错误")
    private String email;
}

 

常见的约束注解如下:

注解

功能

@AssertFalse

可以为null,如果不为null的话必须为false

@AssertTrue

可以为null,如果不为null的话必须为true

@DecimalMax

设置不能超过最大值

@DecimalMin

设置不能超过最小值

@Digits

设置必须是数字且数字整数的位数和小数的位数必须在指定范围内

@Future

日期必须在当前日期的未来

@Past

日期必须在当前日期的过去

@Max

最大不得超过此最大值

@Min

最大不得小于此最小值

@NotNull

不能为null,可以是空

@Null

必须为null

@Pattern

必须满足指定的正则表达式

@Size

集合、数组、map等的size()值必须在指定范围内

@Email

必须是email格式

@Length

长度必须在指定范围内

@NotBlank

字符串不能为null,字符串trim()后也不能等于""

@NotEmpty

不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于""

@Range

值必须在指定范围内

@URL

必须是一个URL

1.3. 定义校验类进行测试

package org.init.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.init.annotation.NotControllerResponseAdvice;
import org.init.common.BaseResponse;
import org.init.common.ResultUtils;
import org.init.model.business.User;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;


@Api(tags = "测试控制器")
@RestController
@RequestMapping("/hello")
public class HelloController {


    @ApiOperation(value = "测试参数控制器")
    @PostMapping("/testParam")
    public BaseResponse testParam(@Validated @RequestBody User user){
        return ResultUtils.success(user);
    }

}

这里我们定义了一个方法testParam函数使用@RequestBody注解,用于接受前端发送得json数据@Validated注解开启校验

1.4. 打开接口文档模拟操作数据

我们故意输入一个错误得邮箱号

 

SpringBoot使用validation实现接口参数统一校验、统一返回值、自定义注解_第1张图片

点击发送请求返回

 

151170ffa7f159ad432f31f9aeff423b.png

报错400是因为 由于邮箱格式出现错误被拦截了

这时我们打开后台查看日志发现后台日志中确实出现了邮箱格式出现错误得提示

 

5f7e1f6582aaa973bbdad7bbe37c5363.png

前端小姐姐如果是这原样展示给用户看,估计代码上午提交得下午产品经理就得找过来找前端小姐姐one by one,显然这样返回给前端肯定是不行滴,于是就要咱们站出来跟前端小姐姐说没问题我来修复包在我身上,没办法谁让咱们是‍呢!

首先先分析一下问题message跑哪里去了,为什么只有在服务端日志才成查看到,并没有包含在响应体中呢?

也是查看了spring boot 版本日志在知道,2.5.x起,默认的异常信息中的message属性被移除了。

如果仍然希望异常响应时显示详细的提示信息,则需要增加如下配置:server: error: include-message: always

 

有了上述配置,message果然就回来了

 

8e14be9a70fb5f457aba54148e8450d5.png

既然我们可以通过配置在异常响应体中增加 message ,那么还有什么其他可配置的信息么?


基于 Spring Boot 2.7.8 ,将异常响应信息做如下梳理:
 

属性名

属性说明

固定 / 可配置

配置项及默认值

timestamp

异常发生时的时间

固定

 

status

http 响应状态码

固定

 

error

与状态码对应的异常原因

固定

 

path

异常发生时的请求路径

固定

 

message

异常的提示信息

可配置

server.error.include-message = never

exception

异常的类名

可配置

server.error.include-exception = false

trace

异常跟踪堆栈信息

可配置

server.error.include-stacktrace = never

errors

BindingResult 中的错误信息

可配置

server.error.include-binding-errors = never

除了 server.error.include-exception 是布尔值外,其它三项配置可选值如下:
 

可选值

配置说明

never

异常响应体中不会包含对应的信息

always

异常响应体中包含对应的信息

on-param

当请求参数中包含相应的参数名(message、trace、errors),且参数值不为 false 时,异常响应体中将包含对应的信息

等等message虽然回来了,但是并没有看到咱们想看到得提示,不行得赶紧处理毕竟牛已经吹出去了

1.5. 参数异常加入全局异常处理器

package org.init.exception;

import org.init.common.BaseResponse;
import org.init.common.ErrorCode;
import org.init.common.ResultUtils;
import org.springframework.validation.BindException;
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;

import java.sql.SQLException;
import java.util.List;

/**
 * spring boot 捕获全局异常
 */
@RestControllerAdvice
public class GlobExceptionHandler {

    @ExceptionHandler(Exception.class)
    public BaseResponse exception(Exception e) {
        Throwable sourceCause = this.getSourceCause(e, e.getCause());
        if (null != sourceCause) {
            if ((sourceCause instanceof SQLException)) {
                return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "执行后台sql语句出现异常!");
            }
            if ((sourceCause instanceof NullPointerException)) {
                return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "空指针异常!");
            }
        }
        return ResultUtils.error(ErrorCode.SYSTEM_ERROR);
    }

    
	//直接监听BindException类就行MethodArgumentNotValidException都是继承得BindException类
    @ExceptionHandler(BindException.class)
    public BaseResponse methodArgumentNotValidException(BindException ex){
        StringBuffer sb = new StringBuffer();
        //遍历所有的错误结果
        for (ObjectError allError : ex.getAllErrors()) {
            sb.append(allError.getDefaultMessage()+"/n");
        }
        return ResultUtils.error(ErrorCode.NULL_ERROR,sb.toString(),"参数不能为空!");
    }


    /**
     * 取得当前异常最初引发来源方
     */
    private Throwable getSourceCause(Exception e, Throwable ex) {

        if ((e instanceof SQLException)) {
            return e;
        }
        if ((e instanceof NullPointerException)) {
            return e;
        }

        if (ex == null)
            return null;

        if (ex.getCause() == null)
            return ex;
        else
            return getSourceCause(e, ex.getCause());

    }
}

增加了全局异常处理这个时候重新发起再看,果然变成了前端小姐姐想要得大功告成,可以向前端小姐姐交差了!

b644891e7037549a9e492d35c172421f.png

 

到这这就是完整得validator框架使用教程了,以为结束了!!!!

然而并没有,现在由于一个项目是多人协作开发得,要是没有标准开发文档显然是不行滴,要是有文档有些小糊涂蛋还是忘记按照规范做返回值咋办呢,毕竟我也是小糊涂蛋中的一员为了弥补有时写错得风险,好!我决定心里暗暗立下falg 如果写错一次返回值给前端小姐姐增加麻烦就,我就饿一天一天不吃饭,人是铁饭是钢,总不吃饭也不行,毕竟身体是革命的本钱,总不能饿着自己吧,为了我能吃上饭,也为了给前端小姐姐留下好印象,我决定把代码给优化一下!!!

往下看!

 

2. spring boot 统一返回值+自定义注解

ResponseBodyAdvice 快速使用

在实际项目中,我们经常需要在请求前后进行一些操作,比如:参数解密/返回结果加密、返回值封装,打印请求参数和返回结果的日志等。这些与业务无关的东西,我们不希望写在controller方法中,造成代码重复可读性变差。这里,我们经常使用@ControllerAdvice和RequestBodyAdvice、ResponseBodyAdvice来对请求前后进行处理(本质上就是AOP),来实现日志记录每一个请求的参数和返回结果。

2.1. 建立统一返回值对象

package org.init.common;

import java.io.Serializable;


/**
 * 通用返回处理对象
 */

public class BaseResponse implements Serializable {

    private int code;

    private T data;

    private String message;

    private String description;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public BaseResponse(int code, T data, String message, String description) {
        this.code = code;
        this.data = data;
        this.message = message;
        this.description = description;
    }

    public BaseResponse(int code, T data) {
        this(code, data, "", "");
    }

    public BaseResponse (int code, T data,String message){
        this(code,data,message,"");
    }

    public BaseResponse(ErrorCode errorCode){
        this(errorCode.getCode(), null, errorCode.getMessage(), errorCode.getMessage());
    }

}

2.2. 建立枚举类

统一状态码

package org.init.common;

/**
 * 错误码
 */
public enum ErrorCode {
    SUCCESS(0, "ok", ""),

    PARAM_ERROR(4000, "参数错误", ""),

    NULL_ERROR(4001, "请求参数为空", ""),

    FORBIDDEN(4002, "无权限", ""),

    SYSTEM_ERROR(5000, "系统内部异常", "");


    private final int code;
    private final String message;

    private final String description;


    ErrorCode(int code, String message, String description) {
        this.code = code;
        this.message = message;
        this.description = description;
    }

    public int getCode() {
        return this.code;
    }

    public String getMessage() {
        return this.message;
    }

    public String getDescription() {
        return this.description;
    }
}

2.3. 建立返回值工具类

package org.init.common;

/**
 * 返回工具类
 */
public class ResultUtils {

    /**
     * 成功
     *
     * @param data
     * @param 
     * @return
     */
    public static  BaseResponse success(T data) {
        return new BaseResponse<>(0, data, "ok");
    }

    /**
     * 失败
     *
     * @param errorCode
     * @return
     */
    public static BaseResponse error(ErrorCode errorCode) {
        return new BaseResponse<>(errorCode);
    }

    /**
     * 失败
     *
     * @param code
     * @param message
     * @param description
     * @return
     */
    public static BaseResponse error(int code, String message, String description) {
        return new BaseResponse(code, null, message, description);
    }

    /**
     * 失败
     *
     * @param errorCode
     * @return
     */
    public static BaseResponse error(ErrorCode errorCode, String message, String description) {
        return new BaseResponse(errorCode.getCode(), null, message, description);
    }

    /**
     * 失败
     *
     * @param errorCode
     * @return
     */
    public static BaseResponse error(ErrorCode errorCode, String description) {
        return new BaseResponse(errorCode.getCode(), errorCode.getMessage(), description);
    }
}

 

2.4. 开始使用ResponseBodyAdvice

1、使用方式:自定义类实现 ResponseBodyAdvice 接口,然后在类上标记 @RestControllerAdvice 注解即可自动识别并进行功能增强。

 

2、下面以对返回数据封装统一格式为例进行演示(注意仅对是有@ResponseBody 注解的控制器方法进行拦截,@RestController 相当于是类中的所有方法上都加了 @ResponseBody)。

 

3、注意如果控制层目标方法往外抛出了异常,则不再进入 ResponseBodyAdvice(需要使用@ExceptionHandler(value = Exception.class))

package org.init.config;

import org.init.annotation.NotControllerResponseAdvice;
import org.init.common.BaseResponse;
import org.init.common.ResultUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
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;

/**
 * spring boot 统一返回值
 */
@RestControllerAdvice
public class ControllerResponseAdvice implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {

        return true;
    }

    //在这里不管什么什么东西都给包装起来
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return ResultUtils.success(body);
    }
}

 

2.5. 编写测试类

package org.init.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.init.annotation.NotControllerResponseAdvice;
import org.init.common.BaseResponse;
import org.init.common.ResultUtils;
import org.init.model.business.User;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;


@Api(tags = "测试控制器")
@RestController
@RequestMapping("/hello")
public class HelloController {



    @ApiOperation(value = "测试参数控制器")
    @PostMapping("/testParam1")
    public User testParam1(@Validated @RequestBody User user){
        return user;
    }

}

2.6. 开始测试

我们ControllerResponseAdvice类给注释掉看一下效果

如果是这样看 还是吃不上饭!

 

SpringBoot使用validation实现接口参数统一校验、统一返回值、自定义注解_第2张图片

我们加上ControllerResponseAdvice再看一下效果

哇哦,这就是我们想要的!

 

SpringBoot使用validation实现接口参数统一校验、统一返回值、自定义注解_第3张图片

思考一个问题,这样增加了ControllerResponseAdvice类确实是比较方便,再也不用担心返回值得问题,随之而来又出现了新的问题,假如前端小姐姐就是要我返回一个字符串怎么办,‍就不能说自己不行,没办法谁让前端是小姐姐呢,干吧!

2.7. 增加一个自定义注解

这个自定义注解是标记在controller 中得方法上的

package org.init.annotation;

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotControllerResponseAdvice {

}

 

2.8. 修改ResponeBodyAdvice类

修改这个方法supports,判断如果返回类型不是咱们得通用返回对象BaseResponse并且方法上没有标注着NotControllerResponseAdvice注解,那么就返回值进行自动包装,如果增加着NotControllerResponseAdvice注解那么就不进行包装,直接返回,完美解决前端小姐姐提出得各种要求

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {

        return !returnType.getParameterType().isAssignableFrom(BaseResponse.class) && !returnType.hasMethodAnnotation(NotControllerResponseAdvice.class);
    }

 

 

 

结语

至此,本次要给大家分享就全部结束了,同时也欢迎业界大佬进行指正!

 

 

你可能感兴趣的:(spring,boot,后端,java)