大家都知道,Spring MVC 默认依赖了 hibernate-validator
校验框架。使用这个,我们可以在可以在model的字段上,加相应的校验注解来轻松的实现数据校验。 例如:
- public class Message implements java.io.Serializable {
-
- private static final long serialVersionUID = 1L;
-
- /** 发送系统编号*/
- @NotEmpty
- private String sysCode;
-
- /** 前置机编号*/
- @NotEmpty
- private String frontName;
-
- /** 前置机时间*/
- @NotEmpty
- @Pattern(regexp = "^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|"
- + "(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|"
- + "[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)\\s+([01][0-9]|"
- + "2[0-3]):[0-5][0-9]:[0-5][0-9]$",message = "must be yyyy-MM-dd HH:mm:ss" )
- private String frontTime;
-
- /** 签名*/
- @NotEmpty
- @JSONField(serialize = false)
- private String sign;
-
- //getter setter
- }
相信大家都有接触过,使用这种方法来实现整体对象的校验,而且还可以根据不同场景,加上不同的 @Group
注解,来实现不同请求对数据的校验规则。
首先我们要想,如何去获得我们需要的参数。我们需要以下参数:
1.请求执行的目标对象
2.请求执行的方法
3.请求的参数
有两种方式来获得:
因为通过拦截器实现,有很多坑要填,这里不推荐使用。主要讲第二个方法,通过AOP来实现校验数据获取。
首先定义一个切面,切入点是所有 controllers
包下所有类的所有方法。 最后我们定义一个方法,在切入点方法之前执行。
当执行到Controller
这一层的时候,所有的数据已经被Spring MVC处理好了,包括数据类型的转换,自定义的WebDataBinder
等。所以我们可以直接通过切面获得所需的校验参数,做最终校验。
- @Aspect
- @Component
- @Order(1)
- public class ValidationAspect {
-
- private transient Logger log = LoggerFactory.getLogger(this.getClass());
-
- @Around("execution(public * *(..)) && @within(org.springframework.validation.annotation.Validated)")
- public Object validateMethodInvocation(ProceedingJoinPoint pjp) throws Throwable {
-
- MethodSignature signature = (MethodSignature) pjp.getSignature();
-
- ExecutableValidator executableValidator = Validation.buildDefaultValidatorFactory().getValidator().forExecutables();
-
- log.info("args:{}",ArrayUtils.toString(pjp.getArgs()));
-
- Set<ConstraintViolation<Object>> validResult = executableValidator.
- validateParameters(pjp.getTarget(), signature.getMethod(), pjp.getArgs());
-
- if (!validResult.isEmpty()) {
- List<FieldError> errors = validResult.stream().map(constraintViolation -> {
- String path = constraintViolation.getPropertyPath().toString();
- int index = path.lastIndexOf('.');
- index = index>0 ? index+1 : 0;
- FieldError error = new FieldError();
- // 参数名称(校验错误的参数名称)
- error.setAttribute(path.substring(index));
- // 校验的错误信息
- error.setErrMsg(constraintViolation.getMessage());
- return error;
- }).collect(Collectors.toList());
-
- throw new ParamValidException(errors);
- }
- Object result = pjp.proceed(); //Execute the method
-
- return result;
- }
-
- }
- package com.somnus.solo.message;
-
- import org.apache.commons.lang3.builder.ToStringBuilder;
- import org.apache.commons.lang3.builder.ToStringStyle;
-
- public class FieldError {
-
- private String attribute;
-
- private String errMsg;
-
- public String getAttribute() {
- return attribute;
- }
-
- public void setAttribute(String attribute) {
- this.attribute = attribute;
- }
-
- public String getErrMsg() {
- return errMsg;
- }
-
- public void setErrMsg(String errMsg) {
- this.errMsg = errMsg;
- }
-
- @Override
- public String toString() {
- return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
- }
-
- }
- package com.somnus.solo.support.exceptions;
-
- import java.util.List;
-
- import com.somnus.solo.message.FieldError;
-
- public class ParamValidException extends RuntimeException {
- private static final long serialVersionUID = 1L;
-
- private List<FieldError> fieldErrors;
-
- public ParamValidException() {
- super();
- }
-
- public ParamValidException(List<FieldError> fieldErrors) {
- this.fieldErrors = fieldErrors;
- }
-
-
- public ParamValidException(Throwable cause) {
- super(cause);
- }
-
- public List<FieldError> getFieldErrors() {
- return fieldErrors;
- }
-
- }
定义控制器增强处理ParamValidException
异常
- @ControllerAdvice
- public class ExceptionAdvice {
-
- @ExceptionHandler({ParamValidException.class})
- @ResponseBody
- public JsonResult handleArrayIndexOutOfBoundsException(ParamValidException e) {
-
- JsonResult result = new JsonResult(false, e.getFieldErrors());
-
- return result;
- }
-
- }
通过这样的方式,我们请求这个方法:
- @RequestMapping(value = "token")
- public JsonResult token(@NotBlank String username, @NotBlank String password){
- String token = userService.login(username, PwdUtils.pwd(password));
- JsonResult result = new JsonResult(token);
- return result;
- }
- 模拟请求不传参数 http://localhsot/token
- {
- "success": false,
- "msg": "invalid params: [`password` 不能为空, `username` 不能为空]",
- "code": 10012,
- "data": [
- {
- "attribute": "password",
- "errMsg": "不能为空"
- },
- {
- "attribute": "username",
- "errMsg": "不能为空"
- }
- ]
- }
2.模拟请求,只传username参数 http://localhost/token?username=testusername
- {
- "success": false,
- "msg": "invalid params: [`password` 不能为空]",
- "code": 10012,
- "data": [
- {
- "attribute": "password",
- "errMsg": "不能为空"
- }
- ]
- }
3.模拟请求,传正确参数 http://localhost/token?username=testusername&password=testpassword
- {
- "success": true,
- "code": 0,
- "data": "token-data"
- }
实现拦截器后,拦截器提供 preHandle
方法,在请求处理之前执行。
- preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
这个方法传过来的最后一个参数 Object handler
实际上是一个 HandlerMethod
对象。
可以通过强制转换获得 HandlerMethod methodHandler = (HandlerMethod) handler
;
通过这个对象,我们可以获取到处理本次请求的处理对象 HandlerMethod.getBean()
,本次请求的处理方法 MethodHandler.getMethod()
。 至此,我们校验需要的前两个参数都有了。
问题就在这最后一个参数上,最后一个参数我们需要获得前端传过来的数据,在这里,我们只能从 HttpServletRequest request 里面获取。
从 request 获取的参数,都只是原始的 String[]
没有经过处理和转换。
如果要实际使用,还需要转换成 方法 对应的数据类型,并考虑自定义的 WebDataBinder
或其复杂类型的数据转换。 相当于要把 Spring MVC 处理参数的逻辑重新实现一遍。虽然也是可以完成的,但是太过于复杂,所以不推荐使用这种方式。