springboot统一表单数据校验
2018-08-29 本文已影响493人
LI木水
统一表单数据校验
在开发中经常需要写一些字段校验的代码,比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉有两个麻烦:
- 验证代码繁琐,重复劳动
- 方法内代码显得冗长
- 每次要看哪些参数验证是否完整,需要去翻阅验证逻辑代码
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
方法将异常写到前台。
测试结果:
参考 https://www.cnblogs.com/mr-yang-localhost/p/7812038.html
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/