参数校验在我们开发过程中非常常见。用户在前端页面上填写表单时,前端js程序会校验参数的合法性,一般前端vue搭配element-ui进行开发时,有一个rule规则可以进行规则校验。当数据到了后端,为了防止恶意操作,保持程序的健壮性,后端同样需要对数据进行校验,此时就需要用到JSR303校验,当然在不管前端还是后端校验,都可能会用到正则表达式。
代码
首先数据库创建如下:随便创建一个用户表,主要通过该表来演示校验规则,比如字段名不能为空,年龄只能为数字等规则。
CREATE TABLE `admin` (
id INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(100) COMMENT '姓名',
`sex` VARCHAR(100) COMMENT '性别',
`age` INT(10) COMMENT '年龄',
`address` VARCHAR(100) COMMENT '地址',
`phone` VARCHAR(100) COMMENT '手机号'
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT '用户表';
INSERT INTO `admin` (`name`,sex,age,address,phone)
VALUES ('张三','男',20,'上海闵行','18011223344');
然后创建一个maven项目,导入如下依赖:需要注意的是校验规则相关的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>mm</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mm</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
然后补充配置文件信息,主要是数据库信息:
server.port=8888
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jsrtest?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8
spring.datasource.username=root
spring.datasource.password=root
然后创建entity, mapper, service, controller以及utils等,项目结构如下:
首先我们创建一个工具类用来统一最后的返回格式:
/**
*
*参考renren开源框架
*
*/
package com.example.mm.utils;
import org.apache.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
/**
* 返回数据
*
* @author Mark sunlightcs@gmail.com
*/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R() {
put("code", 0);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
创建实体类entity: lombok注解
package com.example.mm.entity;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class Admin {
private int id;
private String name;
private int age;
private String sex;
private String address;
private String phone;
}
随后写mapper接口:(随便写了两个方法,查询与添加)
package com.example.mm.mapper;
import com.example.mm.entity.Admin;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AdminMapper {
//查询用户
Admin getAdminById(int id);
//添加用户
boolean addAdmin(Admin admin);
}
对应的在resource目录下创建xml文件:(主要是select和insert的sql,像这种比较简单的sql,也可以通过注解的方式来写,感兴趣的可以去了解一下)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mm.mapper.AdminMapper">
<insert id="addAdmin">
insert into `admin`(`name`,sex,age,address,phone)
values (#{name},#{sex},#{age},#{address},#{phone})
</insert>
<select id="getAdminById" resultType="com.example.mm.entity.Admin">
select id,`name`,sex,age,address,phone from `admin` where id = #{id}
</select>
</mapper>
然后写service层:接口内容与mapper一样
package com.example.mm.service;
import com.example.mm.entity.Admin;
public interface AdminService {
//查询用户
Admin getAdminById(int id);
//添加用户
boolean addAdmin(Admin admin);
}
实现类:
package com.example.mm.service.Impl;
import com.example.mm.entity.Admin;
import com.example.mm.mapper.AdminMapper;
import com.example.mm.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AdminServiceImpl implements AdminService {
@Autowired
private AdminMapper adminMapper;
@Override
public Admin getAdminById(int id) {
Admin admin = adminMapper.getAdminById(id);
return admin;
}
@Override
public boolean addAdmin(Admin admin) {
return adminMapper.addAdmin(admin);
}
}
最后写controller:
package com.example.mm.controller;
import com.example.mm.entity.Admin;
import com.example.mm.service.AdminService;
import com.example.mm.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private AdminService adminService;
//根据id查询用户
@RequestMapping("getAdmin/{id}")
public R getAdminById(@PathVariable("id") Integer id){
Admin admin = adminService.getAdminById(id);
return R.ok().put("data",admin);
}
//添加用户
@RequestMapping("addAdmin")
public R addAdmin(@RequestBody Admin admin){
adminService.addAdmin(admin);
return R.ok();
}
}
最终在postman中测试一下:
首先查询用户
可以看到一条结果显示正确。
添加一条用户信息:
可以看到数据库中确实添加了一条数据。
至此,简单的查询和添加功能就完成了。
然后我们添加如下规则:
(1)用户名,年龄,性别,手机号不能为空
(2)性别只能为男或女或其他
(3)手机号只能有十一位,并且每位数都在0-9
假设就设置这三种规则,首先我们看这几个字段都不能为空,对于非空校验,JSR有三种方式(1) (1) @NotNull: 不能为 null,但可以为 empty,一般用在 Integer 类型的基本数据类型的非空校验上,而且被其标注的字段可以使用 @size、@Max、@Min 对字段数值进行大小的控制
(2) @NotEmpty: 不能为 null,且长度必须大于 0,一般用在集合类上或者数组上
(3) @NotBlank: 只能作用在接收的 String 类型上,注意是只能,不能为 null,而且调用 trim() 后,长度必须大于 0即:必须有实际字符
所以我们在entity中分别对这几个字段加上相应的注解
package com.example.mm.entity;
import lombok.Data;
import javax.validation.constraints.*;
@Data
public class Admin {
private int id;
@NotBlank(message = "用户名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
@Min(value = 0,message = "年龄必须大于等于0")
private int age;
@NotNull
private String sex;
@NotBlank(message = "地址不能为空")
private String address;
@NotEmpty
@Pattern(regexp = "^1[34578]\\d{9}$",message = "手机号十一位必须都在0-9之间")
private String phone;
}
然后在controller中需要在传入的实体上添加@Valid注解(以添加用户为例),不然上述校验规则不会生效。并且通过BindingResult作为结果进行判断,如果有误的话则打印出给定的错误
package com.example.mm.controller;
import com.example.mm.entity.Admin;
import com.example.mm.service.AdminService;
import com.example.mm.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private AdminService adminService;
//根据id查询用户
@RequestMapping("getAdmin/{id}")
public R getAdminById(@PathVariable("id") Integer id){
Admin admin = adminService.getAdminById(id);
return R.ok().put("data",admin);
}
//添加用户
@RequestMapping("addAdmin")
public R addAdmin(@Valid @RequestBody Admin admin, BindingResult result){
if(result.hasErrors()){
Map<String,String> map = new HashMap<>();
result.getFieldErrors().forEach((item) ->{
String message = item.getDefaultMessage(); //获取错误提示
String field = item.getField(); //获取错误的属性名
map.put(field,message);
});
return R.error(400,"数据不合法").put("data",map);
}else{
adminService.addAdmin(admin);
return R.ok();
}
}
}
我们来测试一下,添加用户的时候用户名name我们给个空值,观察最后的结果
从结果可以看出,校验生效,用户名不能为空。
再比如我们随便输入几位手机号(不足11位),结果如下
可以看到,校验同样生效。
其他的字段同样的可以用该方法对其校验规则进行说明。
自定义校验规则
假设对于性别,我们用0和1分别表示男和女,这时我们可以通过自定义校验规则来定义。
这边sex的类型需要换成Integer类型,数据库与实体中都需要改一下
DROP TABLE IF EXISTS `admin`;
CREATE TABLE `admin` (
id INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(100) COMMENT '姓名',
`sex` TINYINT COMMENT '性别',
`age` INT(10) COMMENT '年龄',
`address` VARCHAR(100) COMMENT '地址',
`phone` VARCHAR(100) COMMENT '手机号'
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT '用户表';
INSERT INTO `admin` (`name`,sex,age,address,phone) VALUES ('张三',1,20,'上海闵行','18011223344');
在common包下面新建List自定义注解,ListValueConstrainValidator类等
common结构
package com.example.mm.common;
import javax.validation.Payload;
public @interface List {
String message() default "{性别只能是男或女,用0或1表示}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] vals() default {};
}
package com.example.mm.common;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
public class ListValueConstraintValidator implements ConstraintValidator<List,Integer> {
private Set<Integer> set = new HashSet<>();
//初始化
@Override
public void initialize(List constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for(int val: vals){
set.add(val);
}
}
//判断是否校验成功
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
return set.contains(value);
}
}
同时我们需要将controller层的校验代码拿出来进行封装,以后校验都可以拿过来用
package com.example.mm.common;
import com.example.mm.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice(basePackages = "com.example.mm.controller")
public class MmExpectionControllerAdvice {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleVaildExpection(MethodArgumentNotValidException e){
log.error("数据校验出现问题{},异常类型{}",e.getMessage(),e.getClass());
BindingResult result = e.getBindingResult();
Map<String,String> map = new HashMap<>();
result.getFieldErrors().forEach((fieldError) -> {
map.put(fieldError.getField(),fieldError.getDefaultMessage());
});
return R.error(CodeEnume.VAILD_EXCEPTION.getCode(), CodeEnume.VAILD_EXCEPTION.getMsg()).put("data",map);
}
@ExceptionHandler(value = Throwable.class)
public R handleExpection(Throwable throwable){
log.error("错误",throwable);
return R.error(CodeEnume.UNKNOW_EXCEPTION.getCode(), CodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
状态码
package com.example.mm.common;
/**
* 状态码
*/
public enum CodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败");
private int code;
private String msg;
CodeEnume(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode(){
return code;
}
public String getMsg() {
return msg;
}
}
实体类重新定义sex
package com.example.mm.entity;
import com.example.mm.common.ListValue;
import lombok.Data;
import javax.validation.constraints.*;
@Data
public class Admin {
private int id;
@NotBlank(message = "用户名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
@Min(value = 0,message = "年龄必须大于等于0")
private int age;
@NotNull
@ListValue(vals = {0,1})
private Integer sex; //这边需要改成Integer
@NotBlank(message = "用户名不能为空")
private String address;
@NotEmpty
@Pattern(regexp = "^1[34578]\\d{9}$",message = "手机号十一位必须都在0-9之间")
private String phone;
}
最后运行可以看一下结果:
可以看到自定义校验成功,还有其他的一些校验规则就不做演示了,感兴趣的可以再去了解一下。