Spring Validation框架+AOP实现Controller控制层入参校验

项目开发过程中,通常都涉及到表单提交时候前台传递的表单数据的数据合法性校验,这里说的合法性指的是数据合法性,不涉及业务逻辑上的合法性。为了避免在每个方法中都去写重复的校验逻辑,基于这个目的,使用了spring自带的validation校验框架+aop实现了一个通用的的入参校验处理方法。
这里实现基于springboot1.5.18.RELEASE,java 1.8,项目结构如下:

pom文件引入的依赖:

创建实体类:


import javax.validation.constraints.Max;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;

public class User {
    
    private String id;
    
    //校验字符串是否在指定的范围内
    @Length(min=0, max=20,message = "名字最大长度20")
    private String name;
    
    @Length(min=0, max=100,message = "地址最大长度100")
    private String address;
    
    //email校验,不符合格式则直接返回,为空不校验
    @Email(message = "邮箱格式不正确")
    private String email;
    
    //年龄最大值
    @Max(value = 150,message = "年龄超过最大值150")
    private Integer age;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", name=" + name + ", address=" + address + ", email=" + email + ", age=" + age + "]";
    }
}

自定义注解,用来创建切面时使用:


import java.lang.annotation.*;

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

}

创建切面类:


import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import cst.constant.ResponseConstant;


@Aspect
@Component
public class ParamValidateAspect {
    
    //使用java简单日志门面slf4j,springboot默认使用logback
    private static final Logger log = LoggerFactory.getLogger(ParamValidateAspect.class);
    
    //定义切面要切的方法为所有的带这个注解的方法
    @Pointcut("@annotation(cst.annotation.ParamValidate)")
    public void paramValidate() {}
    
    //切面逻辑
    @Before("paramValidate()")
    public void paramValidateBefore(JoinPoint point) {
        //获取方法入参的数据
        Object[] paramObjs = point.getArgs();
        StringBuffer buffer = new StringBuffer();
        //如果入参个数不为0
        if (paramObjs.length > 0) {
            for (Object object : paramObjs) {
                //如果是BindingResult类型的参数
                if (object instanceof BindingResult) {
                    BindingResult result = (BindingResult)object;
                    //如果有校验失败的信息
                    if (result.hasErrors()) {
                        //循环拼接所有的错误信息
                        List<ObjectError> allErrors = result.getAllErrors();
                        for (ObjectError error : allErrors) {
                            buffer.append(error.getDefaultMessage()+";");
                        }
                    }
                    
                }
            }
        }
        //如果校验信息不为空,则直接返回请求
        String checkResult = buffer.toString();
        if (!StringUtils.isEmpty(checkResult)) {
            //获取到HttpServletResponse对象
            ServletRequestAttributes res = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletResponse response = res.getResponse();
            //设置编码格式
            response.setCharacterEncoding("UTF-8");
            //设置应答头类型
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            //根据实际情况拼接要返回的json字符串,这里因为返回使用了自定义的ReturnData实体,所以拼接成这种实体的json格式
            String returnData = "{" + 
                                    "\"code\":" + "\""+ ResponseConstant.FAILURE_CODE+ "\"" + "," +
                                    "\"msg\":" + "\""+ checkResult + "\"" + "," +
                                    "\"data\":" + "\""+ checkResult + "\""+"," +
                                    "\"pages\": null" + 
                                 "}";
            //将请求返回
            OutputStream output = null;
            try {
                output = response.getOutputStream();
                output.write(returnData.getBytes("UTF-8"));
            } catch (Exception e) {
                log.error(e.getMessage());
            } finally {
                try {
                    if (output != null) {
                        output.close();
                    }
                } catch (IOException e) {
                    log.error(e.getMessage());
                }
            }
        }
    }
}

要使用的返回值的统一实体和返回值常量类:

/**
 * 返回到前台的信息实体
 * @author Administrator
 *
 * @param <T>
 */
public class ReturnData<T> {
    //状态码,0为请求成功,其他均失败
    private Integer code;
    //返回的附加信息
    private String msg;
    //要返回的数据
    private T data;
    
    private Integer pages;
    //无参构造
    public ReturnData() {}
    
    //有参构造
    public ReturnData(int code,String msg,T t){
        this.code=code;
        this.msg=msg;
        this.data=t;
    }
    public ReturnData(int code,String msg,T t,int pages){
        this.code=code;
        this.msg=msg;
        this.data=t;
        this.pages=pages;
    }

    public Integer getCode() {
        return code;
    }

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

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

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

    public Integer getPages() {
        return pages;
    }

    public void setPages(Integer pages) {
        this.pages = pages;
    }

    @Override
    public String toString() {
        return "ReturnData [code=" + code + ", msg=" + msg + ", data=" + data + ", pages=" + pages + "]";
    }
}
/**
 * 返回到前台信息的实体常量类
 */
public class ResponseConstant {
    //请求成功的code
    public static final Integer SUCCESS_CODE=0;
    //请求失败的code
    public static final Integer FAILURE_CODE=1;
    //请求成功的msg
    public static final String SUCCESS_MESSAGE="操作成功!";
    //请求失败的msg
    public static final String FAILURE_MESSAGE="操作失败!";
}

创建的测试controller:



import javax.validation.Valid;

import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cst.annotation.ParamValidate;
import cst.constant.ResponseConstant;
import cst.entity.ReturnData;
import cst.entity.User;


@RestController
@RequestMapping(value = "/user")
public class UserController {
    
    @ParamValidate//这个方法上定义这个注解,切面注解
    @RequestMapping(value = "/saveUser")
    //这里注意使用@Valid 和 BindingResult,这有对应关系
    public ReturnData<String> saveUser(@Valid @RequestBody User user,BindingResult result) {
        return new ReturnData<String>(ResponseConstant.SUCCESS_CODE,ResponseConstant.SUCCESS_MESSAGE,ResponseConstant.SUCCESS_MESSAGE);
    }
}

使用postman模拟表单提交进行测试,请求头设置:

请求返回结果:

常见的注解如下,基本满足需求:
JSR提供的校验注解:
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
Hibernate Validator提供的校验注解:
@NotBlank(message =) 验证字符串非null,且长度必须大于0
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内

以上是一种最简单的实现思路,具体可以根据自己需求进一步优化。

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