springboot统一表单数据校验

统一表单数据校验

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

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

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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348