springboot统一表单数据校验

简介: 统一表单数据校验在开发中经常需要写一些字段校验的代码,比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉有两个麻烦:验证代码繁琐,重复劳动方法内代码显得冗长每次要看哪些参数验证是否完整,需要去翻阅验证逻辑代码hibernate validator(官方文档)提供了一套比较完善、便捷的验证实现方式。

统一表单数据校验

在开发中经常需要写一些字段校验的代码,比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉有两个麻烦:

  • 验证代码繁琐,重复劳动
  • 方法内代码显得冗长
  • 每次要看哪些参数验证是否完整,需要去翻阅验证逻辑代码

hibernate validator(官方文档)提供了一套比较完善、便捷的验证实现方式。

spring-boot-starter-web包里面有hibernate-validator包,不需要引用hibernate validator依赖。

于是编写了一套校验工具类,并配置了切面做统一的校验,无需在每次手动调用校验方法。

校验工具类 ValidatorUtil

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.groups.Default;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * @author lism
 * @date 2018年8月29日10:25:34
 * bean 校验工具类
 */
public class ValidatorUtil {
    private static Validator validator = Validation.buildDefaultValidatorFactory()
            .getValidator();

    /**
     * 校验bean
     * @param bean
     * @param <T>
     * @return
     */
    public static <T> List<ValidateBean> validate(T bean) {
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(bean, Default.class);
        return errors2ValidateBeanList(constraintViolations);
    }

    /**
     * 校验属性
     * @param bean
     * @param property
     * @param <T>
     * @return
     */
    public static <T> List<ValidateBean> validateProperty(T bean, String property) {
        Set<ConstraintViolation<T>> constraintViolations = validator.validateProperty(bean, property, Default.class);
        return errors2ValidateBeanList(constraintViolations);
    }

    /**
     * 校验属性值
     * @param bean
     * @param property
     * @param propertyValue
     * @param <T>
     * @return
     */
    public static <T> List<ValidateBean> validateValue(T bean, String property, Object propertyValue) {
        Set<? extends ConstraintViolation<?>> constraintViolations = validator.validateValue(bean.getClass(), property, propertyValue, Default.class);
        return errors2ValidateBeanList(constraintViolations);
    }

    private static <T> List<ValidateBean> errors2ValidateBeanList(Set<? extends ConstraintViolation<?>> errors) {
        List<ValidateBean> validateBeans = new ArrayList<>();
        if (errors != null && errors.size() > 0) {
            for (ConstraintViolation<?> cv : errors) {
                //这里循环获取错误信息,可以自定义格式
                String property = cv.getPropertyPath().toString();
                String message = cv.getMessage();
                validateBeans.add(new ValidateBean(property, message));
            }
        }
        return validateBeans;
    }
}

校验结果辅助类 ValidateBean

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ValidateBean {
    private String property;
    private String message;

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("{");
        sb.append("property='").append(property).append('\'');
        sb.append(", message='").append(message).append('\'');
        sb.append('}');
        return sb.toString();
    }
}

统一数据校验和异常处理切面

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * 统一数据校验和异常处理
 * 控制层无需再进行数据校验和异常捕获
 * @author lism
 */
@Aspect
@Component
@Slf4j
@Order(1)
public class ValidateBeanAspect {

    /**
     * 定义一个切入点
     */
    @Pointcut("execution(* org.lism..controller..*.*(..))")
    private void anyMethod() {
    }

    @Around("anyMethod()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        if (args.length > 0) {
            Optional<List<ValidateBean>> optional = Arrays.stream(args).filter(arg -> {
                return !(arg == null ||arg instanceof HttpServletRequest || arg instanceof HttpServletResponse);
            }).map(arg -> {
                return ValidatorUtil.validate(arg);
            }).filter(validateBeans -> {
                return validateBeans.size() > 0;
            }).findFirst();
            if (optional.isPresent()) {
                return new ResponseBean(RTCodeEnum.CODE_FAIL.getCode(), optional.get().toString());
            }
        }
        try {
            Object proceed = pjp.proceed(args);
            if (proceed instanceof ResponseBean) {
                return proceed;
            } else {
                return new ResponseBean(proceed, RTCodeEnum.CODE_200);
            }
        } catch (BaseException e) {
            RequestContextUtil.writeToResponse(new ResponseBean<>(e.getCode(), e.getMessage()).toString());
            log.error(e.getMessage());
//            return new ResponseBean<>(e.getCode(), e.getMessage());
            return null;
        } catch (Exception e) {
            log.error(e.getMessage());
            RequestContextUtil.writeToResponse(new ResponseBean<>(RTCodeEnum.CODE_FAIL.getCode(), e.getMessage()));
//            return new ResponseBean<>(RTCodeEnum.CODE_FAIL.getCode(), e.getMessage());
            return null;
        }
    }

    @Before("anyMethod()")
    public void doBefore(JoinPoint pjp) throws Throwable {
    }

    @AfterReturning("anyMethod()")
    public void doAfterReturning(JoinPoint pjp) throws Throwable {
    }
}

RequestContextUtil工具类


import com.alibaba.fastjson.JSON;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * RequestContext工具类
 * @Author lism
 * @Date 2018/8/29 17:04
 */
public class RequestContextUtil {
    public static ServletRequestAttributes getRequestAttributes() {
        return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    }

    /**
     * 获取Request
     * @return
     */
    public static HttpServletRequest getRequest() {
        //TODO:单元测试的时候,还是会得到requestAttributes,不等于null
        ServletRequestAttributes requestAttributes = getRequestAttributes();
        if (requestAttributes != null) {
            return requestAttributes.getRequest();
        } else {
            return null;
        }
    }

    /**
     * 获取Response
     * @return
     */
    public static HttpServletResponse getResponse() {
        //TODO:单元测试的时候,还是会得到requestAttributes,不等于null
        ServletRequestAttributes requestAttributes = getRequestAttributes();
        if (requestAttributes != null) {
            return requestAttributes.getResponse();
        } else {
            return null;
        }
    }

    /**
     * 获取SessionId
     * @return
     */
    public static String getSessionId() {
        //TODO:单元测试的时候,还是会得到requestAttributes,不等于null
        ServletRequestAttributes requestAttributes = getRequestAttributes();
        if (requestAttributes != null) {
            return requestAttributes.getSessionId();
        } else {
            return null;
        }
    }

    /**
     * 往前端写数据
     * @param object
     */
    public static void writeToResponse(Object object) {
        PrintWriter writer = null;
        try {
            HttpServletResponse response = RequestContextUtil.getResponse();
            response.setCharacterEncoding("utf-8");
            response.setHeader("Content-type", "text/html;charset=utf-8");
            writer = response.getWriter();
            writer.write(JSON.toJSONString(object));
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            writer.close();
        }
    }
}

返回code 枚举类

import com.alibaba.fastjson.JSONObject;

/**
 */
public enum RTCodeEnum {


    CODE_OK(0, "OK"), //
    CODE_DONE(1, "Done"), //
    CODE_FAIL(-1, "Failed"),

    CODE_PARAM_ERROR(300, "Input Param Error"),

    CODE_TOKEN_ERROR(301, "Token Validation Error"),

    CODE_CAPTCHA_ERROR(302, "验证码错误,请重试"),

    CODE_DATA_VALIDATE_FAILED(303, "数据校验未通过"),


    CODE_STATE_EXIST(305, "请勿重复请求"),
    // Data Issue: 4**
    CODE_400(400, "服务404"),

    CODE_DATA_ERROR_PAGETIME_EXPIRE(401, "页面超时不可用,请刷新重试"),

    // System Service Issue: 5**
    CODE_SERVICE_NOT_AVAILABLE(500, "系统服务不可用,请联系管理员"),
    CODE_200(200,"成功"),

    CODE_401(401,"未登录,需要登录"),

    CODE_405(405, "权限不足"),

    CODE_406(406,"客户端请求接口参数不正确或缺少参数"),

    CODE_501(501,"服务器接口错误"),

    CODE_999(999,"保留码");


    private int code;
    private String desc;


    RTCodeEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public JSONObject toJSON() {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", code);
        jsonObject.put("desc", desc);
        return jsonObject;
    }

    public int getCode() {
        return code;
    }

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

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

ResponseBean工具类


import com.alibaba.fastjson.annotation.JSONField;
import com.google.common.base.MoreObjects;

import java.util.Date;

/**
 * Created by lism 2018/8/23.
 */
public class ResponseBean<T> {
    private int code = 200;
    private String msg;
    private T data;

    @JSONField(format="yyyy-MM-dd HH:mm:ss")
    private Date date = new Date();


    public static ResponseBean me(Object data){
        return new ResponseBean(data);
    }

    public ResponseBean(T data) {
        this.data = data;
    }

    public ResponseBean(T data, RTCodeEnum rtCodeEnum) {
        this.data = data;
        this.msg = rtCodeEnum.getDesc();
        this.code = rtCodeEnum.getCode();
    }
    public ResponseBean(RTCodeEnum rtCodeEnum) {
        this.msg = rtCodeEnum.getDesc();
        this.code = rtCodeEnum.getCode();
    }

    public ResponseBean(T data, int code, String msg) {
        this.data = data;
        this.code = code;
        this.msg = msg;
    }

    public ResponseBean(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ResponseBean() {
    }

    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;
    }

    public T getData() {
        return data;
    }

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

    public Date getDate() {
        return date;
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("code", code)
                .add("msg", msg)
                .add("data", data)
                .toString();
    }
}

1.校验实体类型参数

编写测试实体

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.validation.constraints.NotEmpty;

@Entity
@Table(name = "t_subject")
@Data
public class Subject extends BaseEntityModel {

    @NotEmpty(message = "专题名不能为空")
    @Length(min = 1, max = 20, message = "专题名1-20个字符之间")
    private String name;
    private String keyword;
    private String[] docType;
    private String startTime;
    private String endTime;
}

测试控制器

@RestController
@RequestMapping(value = "/subject")
public class SubjectController {

    @RequestMapping(value = "/", method = RequestMethod.POST, produces = "application/json", consumes = "application/json")
    @ResponseBody
    @Override
    public ResponseBean create(@RequestBody Subject model) {
   
        return super.create(model);
    }

输入一个空的名子进行测试,可以看到返回结果


v2.png

2.校验RequestParam 类型请求参数

ValidatorUtil方式无法校验RequestParam 请求方法,在BaseController 加上@Validated注解

@RestController
@Validated
public class BaseController {
}

在子控制器写测试方法

@RestController
@RequestMapping(value = "/subject")
public class SubjectController extends BaseController {
  
    /**
     * test4
     * @return
     */
    @RequestMapping(value = "/test3/", method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    public void test3(HttpServletRequest request, HttpServletResponse response,
                      @Length(min = 1, max = 20, message = "专题名1-20个字符之间")
                      @RequestParam String name) {
        log.info("test4");
    }

通过测试看到:验证不通过时,抛出了ConstraintViolationException异常,所以我们在ValidateBeanAspect中使用统一的捕获异常是可以捕获到异常,并调用RequestContextUtil.writeToResponse方法将异常写到前台。
测试结果:

v1.png

参考 https://www.cnblogs.com/mr-yang-localhost/p/7812038.html
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/

目录
相关文章
|
7月前
|
NoSQL Java Redis
SpringBoot集成Redis解决表单重复提交接口幂等(亲测可用)
SpringBoot集成Redis解决表单重复提交接口幂等(亲测可用)
508 0
|
26天前
|
安全 Java 数据安全/隐私保护
如何使用Spring Boot进行表单登录身份验证:从基础到实践
如何使用Spring Boot进行表单登录身份验证:从基础到实践
43 5
|
4月前
|
JSON JavaScript 前端开发
基于SpringBoot + Vue实现单个文件上传(带上Token和其它表单信息)的前后端完整过程
本文介绍了在SpringBoot + Vue项目中实现单个文件上传的同时携带Token和其它表单信息的前后端完整流程,包括后端SpringBoot的文件上传处理和前端Vue使用FormData进行表单数据和文件的上传。
279 0
基于SpringBoot + Vue实现单个文件上传(带上Token和其它表单信息)的前后端完整过程
|
4月前
|
前端开发 JavaScript Java
SpringBoot+JQuery+Ajax实现表单数据传输和单文件或多文件的上传
关于如何在SpringBoot项目中结合JQuery和Ajax实现表单数据的传输以及单文件或多文件上传的教程。文章提供了完整的前后端示例代码,包括项目的`pom.xml`依赖配置、SpringBoot的启动类`App.java`、静态资源配置`ResourceConfig.java`、配置文件`application.yml`、前端HTML页面(单文件上传和多文件上传加表单内容)以及后端控制器`UserController.java`。文章最后展示了运行结果的截图。
279 0
SpringBoot+JQuery+Ajax实现表单数据传输和单文件或多文件的上传
|
5月前
|
存储 Java Spring
Spring Boot中的表单处理
Spring Boot中的表单处理
|
6月前
|
存储 开发框架 Java
Spring Boot中的表单处理
Spring Boot中的表单处理
|
数据挖掘 Java 测试技术
无代码动态表单系统 毕业设计 JAVA+Vue+SpringBoot+MySQL(一)
无代码动态表单系统 毕业设计 JAVA+Vue+SpringBoot+MySQL
181 0
Spring Boot 一个接口同时支持 form 表单、form-data、json 优雅写法
网上很多代码都是千篇一律的 cvs,相信我只要你认真看完我写的这篇,你就可以完全掌握这个知识点,这篇文章不适合直接 cvs,一定要先理解。
|
7月前
|
Java
解决springboot添加拦截器之后只能获取一次流,并且@requestbody注解和表单方式都可以接到参
解决springboot添加拦截器之后只能获取一次流,并且@requestbody注解和表单方式都可以接到参
|
7月前
|
NoSQL Java API
SpringBoot项目中防止表单重复提交的两种方法(自定义注解解决API接口幂等设计和重定向)
SpringBoot项目中防止表单重复提交的两种方法(自定义注解解决API接口幂等设计和重定向)
577 0