Spring boot2.x-第03讲:数据校验
有啥子问题欢迎各路神仙指点迷津。
1.前言
在看本篇文章之前,推荐大家先温习一下以下内容:
- SpringBoot 通过自定义注解实现AOP切面编程实例
- spring boot的@RequestParam和@RequestBody的区别
- 注解@RequestParam与@RequestBody的使用场景
- Spring boot2.x-全局异常处理
- Swagger注解释义与SpringBoot的集成 [这个不是特别重要]
2.数据校验
2.1 诉求
在web开发中,针对请求参数,一般需要进行参数合法化校验,原本写法是一个一个字段判断,这种方式不通用且繁琐了,Java的JSR 303: Bean Validation
规范就是解决这个问题的。
上代码……
2.2 API接口上送模型
package com.tong.hao.online.api.model.test;
import com.tong.hao.common.entity.api.TongBaseRequest;
import com.tong.hao.online.api.model.RegexpConstant;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Setter
@Getter
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
public class DemoReq extends TongBaseRequest {
private static final long serialVersionUID = 1865606619170433347L;
@ApiModelProperty(value = "姓名", required = true, position = 0)
// @NotBlank(message = "name不能为空")
// 该注解不需要写message,在/resources/ValidationMessages.properties文件中统一配置
@NotBlank
private String name;
@ApiModelProperty(value = "年龄", position = 1)
@Min(value = 1, message = "{demoReq.age}")
private String age;
@ApiModelProperty(value = "性别", position = 2, allowableValues = "I|U|D")
// 使用@Pattern可以实现属性枚举值校验
@Pattern(regexp = RegexpConstant.TEST_DEMO_ENUM)
private String sex;
}
【注意】:该模型不用@Date注解,因为TongBaseRequest类中重写了toString()方法,如果使用@Date注解的话,也会重写toString()方法。
DemoReq模型中@Min注解的message指的是当age<1时的异常信息,@NotBlank注解的默认message为"javax.validation.constraints.NotBlank.message"(查看源码可知,此处在配置文件中覆盖异常信息),demeReq.age在/resources/ValidationMessages.properties文件中配置,文件内容如下:
javax.validation.constraints.NotBlank.message=不能为空
demoReq.age=age年龄必须大于等于1
2.3 Controller测试类
@Api(tags = {"测试控制器"})
@RestController
@Slf4j
@RequestMapping(value = "/test")
public class TestController implements ITestController {
/**
* <pre>
* 接口数据校验测试,post请求
* </pre>
*/
@Override
@ApiOperation(value = "数据校验测试-POST")
@RequestMapping(method = RequestMethod.POST, value = VALID_POST)
public String demoValidOfPost(@Valid @RequestBody DemoReq demoReq) {
System.err.println("数据校验测试-POST");
Map<String, Object> result = new HashMap<>();
result.put("姓名", demoReq.getName());
result.put("年龄", demoReq.getAge());
return result.toString();
}
}
【注意】:
方法demoValidOfPost()的入参必须要有@Valid注解
;方法入参必须要有@RequestBody
;方法入参必须只有一个对象
。
2.4 第一次测试
请求报文如下
{
"tongInHead":{
"clientNo":"123",
"reqSystemId":"TWX",
"reqBusinessNo":"123456789",
"reqTranDate":"2019-09-28",
"reqControllerUrl":"/test/valid/post"
},
"name":"呵呵",
"age":"-8"
}
响应报文如下
{
"tongOutHead": {
"exceptionResultList": [
{
"retCode": "999999",
"retMsg": "业务执行异常!"
}
],
"timestamp": "2019-09-28 20:35:09.975"
}
}
log日志如下
org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.tong.hao.online.service.controller.test.TestController.demoValidOfPost(com.tong.hao.online.api.model.test.DemoReq): [Field error in object 'demoReq' on field 'age': rejected value [-8]; codes [Min.demoReq.age,Min.age,Min.java.lang.String,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [demoReq.age,age]; arguments []; default message [age],1]; default message [age年龄必须大于等于1]]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:138) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:127) ~[spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167) ~[spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134) ~[spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005) [spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908) [spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
【Tip】:响应报文
貌似不太友好啊,改进……
2.5 进阶一
从2.3的日志中可以看出报错的异常类是org.springframework.web.bind.MethodArgumentNotValidException
,再根据上一篇文章 Spring boot2.x-全局异常处理 可以考虑对该异常进行全局处理。对GlobalExceptionHandler类进行修改如下,添加代码2- 处理 @RequestBody注解方式的参数校验
package com.tong.hao.online.service.handler;
import com.tong.hao.common.entity.api.TongBaseResponse;
import com.tong.hao.common.exception.BusinessException;
import com.tong.hao.common.exception.BusinessResult;
import com.tong.hao.common.exception.constants.ExceptionEnum;
import com.tong.hao.common.utils.ThBeanUtils;
import com.tong.hao.common.utils.ThStringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @ClassName GlobalExceptionHandler
* @Author 友野浩二
* @Description 全局的统一异常处理类
*
* <pre>
* controller增强器:
* @ControllerAdvice(controller增强器) :
* 被@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法,都会作用在被 @RequestMapping注解的方法上。
* @ExceptionHandler(异常处理器) :
* 此注解的作用是当出现其定义的异常时进行处理的方法。
*
* RestControllerAdvice = @ControllerAdvice + @ResponseBody,该类中所有的方法都会返回json格式的数据。
* 如果异常要返回也页面,则就使用@ControllerAdvice。
* </pre>
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* ExceptionHandler拦截了异常,我们可以通过该注解实现自定义异常处理。其中,@ExceptionHandler 配置的 value 指定需要拦截的异常类型。
*/
@ExceptionHandler(value = {Exception.class})
// @ResponseBody
public TongBaseResponse handlerException(Exception ex) {
log.info("com.tong.hao.online.service.handler.GlobalExceptionHandler.handlerException");
log.info(ex.getMessage(), ex);
// 1- 处理 业务异常处理
if (ex instanceof BusinessException) {
BusinessException businessException = (BusinessException) ex;
return BusinessResult.error(businessException.getExceptionCode(), businessException.getExceptionMessage());
}
// 2- 处理 @RequestBody注解方式的参数校验
if (ex instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) ex;
BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
FieldError fieldError = bindingResult.getFieldError();
if (!ThBeanUtils.isNull(fieldError)) {
log.info("MethodArgumentNotValidException, {}, {}", fieldError.getField(), fieldError.getDefaultMessage());
return BusinessResult.error(ExceptionEnum.FILED_ERROR.getCode(), getMessage(fieldError));
}
}
// 其他异常处理
return BusinessResult.error(ExceptionEnum.FAILED.getCode(), ExceptionEnum.FAILED.getMessage());
}
private String getMessage(FieldError fieldError) {
String message;
try {
// [字段名] + 错误消息
message = "[".concat(fieldError.getField()).concat("]").concat(ThStringUtils.nvl(fieldError.getDefaultMessage(), ""));
} catch (Exception e) {
message = fieldError.getDefaultMessage();
}
return message;
}
}
2.6 第二次测试
请求报文不变
响应报文如下
{
"tongOutHead": {
"exceptionResultList": [
{
"retCode": "TH000",
"retMsg": "[age]age年龄必须大于等于1"
}
],
"timestamp": "2019-09-28 20:48:27.588"
}
}
log日志如下
堆栈信息没变,可以看到代码(62行代码)中打印的日志如下
MethodArgumentNotValidException, age, age年龄必须大于等于1
【Tip】:响应报文是友好了,but,打印的日志还是很恶心
的,继续改进……
2.7 进阶二
看到这里,就需要用到切面了,这里我们在切面的前置通知中对API接口的入参进行手动校验,切面代码如下
package com.tong.hao.online.service.aspect;
import com.google.common.base.Stopwatch;
import com.tong.hao.common.entity.api.TongBaseRequest;
import com.tong.hao.common.exception.constants.ExceptionEnum;
import com.tong.hao.common.utils.BusinessUtils;
import com.tong.hao.common.utils.ThBeanUtils;
import com.tong.hao.common.utils.ValidatorUtils;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @Author 友野浩二
* @ClassName ValidateRequestAspect
* @Description // 切面: 接口数据校验
* @Date 2019/9/25 21:46
* @Version 1.0
*
* <pre>
* @Lazy 注解: 容器一般都会在启动的时候实例化所有单实例bean,如果我们想要Spring在启动的时候延迟加载bean,需要用到这个注解。
* value值为true 或 false,默认true(延迟加载),@Lazy(value = false)表示对象会在初始化的时候创建。
* </pre>
**/
@Aspect
@Component
@Lazy(value = false)
@Slf4j
public class ValidateRequestAspect {
private static ThreadLocal<Long> startTime = new ThreadLocal<>();// 记录API接口执行的开始时间
/*
* @Author 友野浩二
* @Date 2019/9/25 22:13
* @Param []
* @return void
* @Description // 定义切入点:要对哪些类中的哪些方法进行增强,进行切割,指的是被增强的方法。即要切哪些东西。如包、类。
*
* <pre>
* execution: 用来匹配执行方法的连接点。
* 1- 语法结构: execution(方法修饰符 方法返回值类型[必填] 方法所属包和类 匹配方法名(方法中的形参) 方法申明抛出异常)
* execution(modifiers-pattern?ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throw-pattern?)
* 2- 各部分都支持通配符"*"来匹配全部(可以使用..表示当前包及子包);
* 方法的形参支持两种通配符。
* "*": 代表[一个]任意类型的参数;
* "..": 代表零个或多个任意类型的参数.
*
* 当前方法的切入点是:com.tong.hao.online.service.controller包及子包下的所有类的 接收一个参数的方法
* 【注意】:开发规范规定controller类的所有方法只能有一个参数。
* </pre>
**/
@Pointcut("execution(* com.tong.hao.online.service.controller..*.*(*))")
// @Pointcut("execution(public String com.tong.hao.online.service.controller.test.TestController.demoValidOfPost(..))")
private void validator() {
log.info("<======== entry ValidateRequestAspect validator ========>");
}
/**
* @Author 友野浩二
* @Description // 前置通知:在目标方法执行前调用,在该方法中完成API接口入参模型的数据校验(类似javax.validation.constraints.NotBlank这种校验)
* @Date 2019/9/25 23:05
**/
@Before("validator()")
public void doBeforeController(JoinPoint joinPoint) {
log.info("<======== entry ValidateRequestAspect 前置通知 ========>");
// 在前置通知中进行接口入参数据校验
Stopwatch stopwatch = Stopwatch.createStarted();// 创建自动start的计时器
Object[] args = joinPoint.getArgs();// 获取传入目标方法的参数对象
TongBaseRequest tongBaseRequest = (TongBaseRequest) args[0];// 因为validator()方法的切入点配置的目标方法只有一个参数,所以只获取参数对象的第一个
log.info("request msg: {}", tongBaseRequest);// 打印请求报文
Validator validator = ValidatorUtils.getValidator();
// 对入参对象作校验,如果校验成功,则返回的 ConstraintViolation 类型的集合为空,否则该集合为空;
// 集合中的每一个元素(ConstraintViolation 类型)对应一个违反的约束。
Set<ConstraintViolation<TongBaseRequest>> constraintViolationSet = validator.validate(tongBaseRequest);
if (!ThBeanUtils.isNull(constraintViolationSet)) {
ConstraintViolation<TongBaseRequest> violationInfo = constraintViolationSet.iterator().next();
String message;
try {
ApiModelProperty apiModelProperty = violationInfo.getRootBeanClass()
.getDeclaredField(violationInfo.getPropertyPath().toString())
.getAnnotation(ApiModelProperty.class);
message = "[".concat(violationInfo.getPropertyPath().toString())
.concat(", ").concat(apiModelProperty.value()).concat("]")
.concat(violationInfo.getMessage());// 属性名 + ApiModelProperty.value(属性说明) + 异常信息
} catch (NoSuchFieldException e) {
message = violationInfo.getMessage();
}
throw BusinessUtils.createBusinessException(ExceptionEnum.FILED_ERROR.getCode(), message);// API接口数据校验的错误码统一为TH000
}
log.info("<======== exit ValidateRequestAspect 前置通知, elapsed time is [{}] ms ========>",
stopwatch.elapsed(TimeUnit.MILLISECONDS));
startTime.set(System.currentTimeMillis());// 设置当前时间戳
}
/**
* @Author 友野浩二
* @Description // 后置通知:在目标方法执行后调用,若目标方法出现异常,则不执行。
* @Date 2019/9/25 23:10
*
* <pre>
* @AfterReturning 注解属性说明:
* pointcut/value:这两个属性的作用是一样的,它们都属于指定切入点对应的表达式。当指定了
* pointcut属性值后,value属性值会被覆盖;
* returning:该属性指定一个形参名,用于表示Advice方法中可定义与此同名的形参,该形参可用
* 于访问目标方法的返回值。除此之外,在Advice方法中定义该形参(代表目标方法的
* 返回值)时指定的类型,会限制目标方法必须返回指定类型的值或没有返回值。若声
* 明为Object,则意味着对目标方法的返回值不加限制。
* </pre>
**/
@AfterReturning(pointcut = "validator()", returning = "responseValue")
public void doAfterReturningController(Object responseValue) {
log.info("<======== entry ValidateRequestAspect 后置通知: after returning ========>");
log.info("response msg: {}", responseValue);
Long costTime = System.currentTimeMillis() - startTime.get();
log.info("<======== 当前API接口耗费时间:{} ms", costTime);
}
/**
* @Author 友野浩二
* @Description // 后置/最终通知:无论目标方法在执行过程中是否出现异常,都会在它之后调用
* @Date 2019/9/25 23:13
**/
@After("validator()")
public void doAfterController() {
log.info("<======== entry ValidateRequestAspect 后置通知: finally returning ========>");
}
}
【注意】:
- 2.5进阶一中全局异常处理类中关于
MethodArgumentNotValidException
的代码可以注释掉; Controller测试类中的方法入参@Valid注解必须去掉
,否则切面切不到的;- 启动类上需要加入注解
@EnableAspectJAutoProxy(proxyTargetClass = true)
开启增强代理(AOP切面)
ValidatorUtils工具类
package com.tong.hao.common.utils;
import org.hibernate.validator.HibernateValidator;
import javax.validation.Validation;
import javax.validation.Validator;
/**
* @ClassName ValidatorUtils
* @Author 友野浩二
* @Description 接口controller数据校验工具类
* @Version 1.0
*/
public class ValidatorUtils {
// 使用HibernateValidator
private static Validator validator = Validation.byProvider(HibernateValidator.class).configure()
.failFast(true)
.buildValidatorFactory()
.getValidator();
public static Validator getValidator() {
return validator;
}
}
2.8 第三次测试
请求报文不变
响应报文不变
log日志如下
com.tong.hao.common.exception.BusinessException: [{"exceptionCode" : "TH000", "exceptionMessage" : "[age, 年龄]age年龄必须大于等于1"}]
at com.tong.hao.common.utils.BusinessUtils.createBusinessException(BusinessUtils.java:22) ~[classes/:na]
at com.tong.hao.online.service.aspect.ValidateRequestAspect.doBeforeController(ValidateRequestAspect.java:96) ~[classes/:na]
可以看到异常信息已经是自定义异常了。