Springboot使用@Valid 和AOP做参数校验及日志输出问题
项目背景
最近在项目上对接前端的的时候遇到了几个问题
1.经常要问前端要请求参数
2.要根据请求参数写大量if...else,代码散步在Controller中,影响代码质量
3.为了解决问题1,到处记日志,导致到处改代码
解决方案
为了解决这类问题,我使用了@Valid做参数校验,并使用AOP记录前端请求日志
1.Bean实体类增加注解
对要校验的实体类增加注解,如果实体类中有List结构,就在List上加@Valid
@Valid注解
注解 | 备注 |
---|---|
@Null | 只能为null |
@NotNull | 必须不为null |
@Max(value) | 必须为一个不大于value的数字 |
@Min(value) | 必须为一个不小于value的数字 |
@AssertFalse | 必须为false |
@AssertTrue | 必须为true |
@DecimalMax(value) | 必须为一个小于等于value的数字 |
@DecimalMin(value) | 必须为一个大于等于value的数字 |
@Digits(integer,fraction) | 必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Past | 必须是日期,且小于当前日期 |
@Future | 必须是日期 ,且为将来的日期 |
@Size(max,min) | 字符长度必须在min到max之间 |
@Pattern(regex=,flag=) | 必须符合指定的正则表达式 |
@NotEmpty | 必须不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 必须不为空(不为null、去除首位空格后长度不为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
必须为Email,也可以通过正则表达式和flag指定自定义的email格式 |
UserInfo
packagecom.zero.check.query; importlombok.Data; importorg.hibernate.validator.constraints.EAN; importorg.springframework.stereotype.Component; importjavax.validation.Valid; importjavax.validation.constraints.*; importjava.util.List; /** *@Description: *@author:wei.wang *@since:2019/11/2115:05 *@history:1.2019/11/21createdbywei.wang */ @Component @Data publicclassUserInfo{ @NotBlank(message="主键不能为空") @Pattern(regexp="^[1-9]\\d*$",message="主键范围不正确") privateStringid; @Valid @NotEmpty(message="用户列表不能为空") privateListuserList; @NotNull(message="权限不能为空") @Min(value=1,message="权限范围为[1-99]") @Max(value=99,message="权限范围为[1-99]") privateLongroleId; }
User
packagecom.zero.check.query; importlombok.Data; importorg.springframework.stereotype.Component; importjavax.validation.constraints.NotBlank; importjavax.validation.constraints.NotNull; importjava.util.List; /** *@Description: *@author:wei.wang *@since:2019/11/2116:03 *@history:1.2019/11/21createdbywei.wang */ @Component @Data publicclassUser{ @NotBlank(message="用户工号不能为空") privateStringuserId; @NotBlank(message="用户名称不能为空") privateStringuserName; publicStringgetUserId(){ returnuserId; } publicvoidsetUserId(StringuserId){ this.userId=userId; } publicStringgetUserName(){ returnuserName; } publicvoidsetUserName(StringuserName){ this.userName=userName; } }
2.Controller层
在需要校验的pojo前边添加@Validated,在需要校验的pojo后边添加BindingResultbr接收校验出错信息,需要注意的是,BindingResultresult一定要跟在@Validated注解对象的后面(必须是实体类),而且当有多个@Validated注解时,每个注解对象后面都需要添加一个BindingResult,而实际使用时由于在WebLogAspect切点读取了请求数据,会导致在Controller层请求参数中读不到数据,这里需要修改其他内容,详见Git
DataCheckController packagecom.zero.check.controller; importcom.zero.check.query.User; importcom.zero.check.query.UserInfo; importcom.zero.check.utils.Response; importorg.springframework.validation.BindingResult; importorg.springframework.web.bind.annotation.*; importjavax.validation.Valid; /** *@Description: *@author:wei.wang *@since:2019/11/2114:57 *@history:1.2019/11/21createdbywei.wang */ @RestController @RequestMapping(value="/check") publicclassDataCheckController{ @PostMapping(value="/userValidPost") publicResponsequeryUserPost(@Valid@RequestBodyUserInfouserInfo,BindingResultresult){ returnResponse.ok().setData("Hello"+userInfo.getId()); } @GetMapping(value="/userValidGet") publicResponsequeryUserGet(@ValidUseruser,BindingResultresult){ returnResponse.ok().setData("Hello"+user.getUserName()); } }
3.AOP
定义切点@Pointcut("execution( com.zero.check.controller..(..))"),定义后可监控com.zero.check.controller包和子包里任意方法的执行
如果输入参数不能通过校验,就直接抛出异常,由于定义了UserInfoHandler拦截器,可以拦截处理校验错误,这样就可以省略大量的非空判断,让Controller层专注业务代码,并且将日志集中在WebLogAspect中处理,不会因为记录日志导致要到处改代码
if(bindingResult.hasErrors()){ FieldErrorerror=bindingResult.getFieldError(); thrownewUserInfoException(Response.error(error.getDefaultMessage()).setData(error)); } UserInfoHandler packagecom.zero.check.handler; importcom.zero.check.exception.UserInfoException; importorg.springframework.web.bind.annotation.ExceptionHandler; importorg.springframework.web.bind.annotation.RestControllerAdvice; /** *@Description: *@author:wei.wang *@since:2019/11/2115:04 *@history:1.2019/11/21createdbywei.wang */ @RestControllerAdvice publicclassUserInfoHandler{ /** *校验错误拦截处理 * *@parame错误信息集合 *@return错误信息 */ @ExceptionHandler(UserInfoException.class) publicObjecthandle(UserInfoExceptione){ returne.getR(); } }
WebLogAspect
packagecom.zero.check.aspect; importcom.alibaba.fastjson.JSON; importcom.zero.check.exception.UserInfoException; importcom.zero.check.utils.Response; importorg.aspectj.lang.JoinPoint; importorg.aspectj.lang.annotation.*; importorg.slf4j.Logger; importorg.slf4j.LoggerFactory; importorg.springframework.stereotype.Component; importorg.springframework.validation.BindingResult; importorg.springframework.validation.FieldError; importorg.springframework.web.context.request.RequestAttributes; importorg.springframework.web.context.request.RequestContextHolder; importjavax.servlet.http.HttpServletRequest; importjava.io.BufferedReader; importjava.io.IOException; importjava.io.InputStream; importjava.io.InputStreamReader; importjava.util.Enumeration; importjava.util.HashMap; importjava.util.Map; importjava.util.Optional; /** *@Description: *@author:wei.wang *@since:2019/11/2113:47 *@history:1.2019/11/21createdbywei.wang */ @Aspect @Component publicclassWebLogAspect{ privateLoggerlogger=LoggerFactory.getLogger(WebLogAspect.class); privatefinalStringREQUEST_GET="GET"; privatefinalStringREQUEST_POST="POST"; /** *定义切点,切点为com.zero.check.controller包和子包里任意方法的执行 */ @Pointcut("execution(*com.zero.check.controller..*(..))") publicvoidwebLog(){ } /** *前置通知,在切点之前执行的通知 * *@paramjoinPoint切点 */ @Before("webLog()&&args(..,bindingResult)") publicvoiddoBefore(JoinPointjoinPoint,BindingResultbindingResult){ if(bindingResult.hasErrors()){ FieldErrorerror=bindingResult.getFieldError(); thrownewUserInfoException(Response.error(error.getDefaultMessage()).setData(error)); } //获取请求参数 try{ StringreqBody=this.getReqBody(); logger.info("REQUEST:"+reqBody); }catch(Exceptionex){ logger.info("getRequestError:"+ex.getMessage()); } } /** *后置通知,切点后执行 * *@paramret */ @AfterReturning(returning="ret",pointcut="webLog()") publicvoiddoAfterReturning(Objectret){ //处理完请求,返回内容 try{ logger.info("RESPONSE:"+JSON.toJSONString(ret)); }catch(Exceptionex){ logger.info("getResponseError:"+ex.getMessage()); } } /** *返回调用参数 * *@returnReqBody */ privateStringgetReqBody(){ //从获取RequestAttributes中获取HttpServletRequest的信息 HttpServletRequestrequest=this.getHttpServletRequest(); //获取请求方法GET/POST Stringmethod=request.getMethod(); Optional.ofNullable(method).orElse("UNKNOWN"); if(REQUEST_POST.equals(method)){ returnthis.getPostReqBody(request); }elseif(REQUEST_GET.equals(method)){ returnthis.getGetReqBody(request); } return"getRequestParameterError"; } /** *获取request *Spring对一些(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态的bean采用ThreadLocal进行处理 *让它们也成为线程安全的状态 * *@return */ privateHttpServletRequestgetHttpServletRequest(){ //获取RequestAttributes RequestAttributesrequestAttributes=RequestContextHolder.getRequestAttributes(); return(HttpServletRequest)requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST); } /** *获取GET请求数据 * *@paramrequest *@return */ privateStringgetGetReqBody(HttpServletRequestrequest){ Enumerationenumeration=request.getParameterNames(); Map parameterMap=newHashMap<>(16); while(enumeration.hasMoreElements()){ Stringparameter=enumeration.nextElement(); parameterMap.put(parameter,request.getParameter(parameter)); } returnparameterMap.toString(); } /** *获取POST请求数据 * *@paramrequest *@return返回POST参数 */ privateStringgetPostReqBody(HttpServletRequestrequest){ StringBuilderstringBuilder=newStringBuilder(); try(InputStreaminputStream=request.getInputStream(); BufferedReaderbufferedReader=newBufferedReader(newInputStreamReader(inputStream))){ char[]charBuffer=newchar[128]; intbytesRead=-1; while((bytesRead=bufferedReader.read(charBuffer))>0){ stringBuilder.append(charBuffer,0,bytesRead); } }catch(IOExceptione){ logger.info("getPostRequestParametererr:"+e.getMessage()); } returnstringBuilder.toString(); } }
4.测试
POST接口
localhost:9004/check/userValidPost
请求参数
{ "id":"12", "userList":[ { "userId":"Google", "userName":"http://www.google.com" }, { "userId":"S", "userName":"http://www.SoSo.com" }, { "userId":"SoSo", "userName":"http://www.SoSo.com" } ], "roleId":"11" }
返回结果
{ "code":"ok", "data":"Hello12", "requestid":"706cd81db49d4c9795e5457cebb1ba8c" }
请求参数
{ "id":"1A2", "userList":[ { "userId":"Google", "userName":"http://www.google.com" }, { "userId":"S", "userName":"http://www.SoSo.com" }, { "userId":"SoSo", "userName":"http://www.SoSo.com" } ], "roleId":"11" }
返回结果
{ "code":"error", "message":"主键范围不正确", "data":{ "codes":[ "Pattern.userInfo.id", "Pattern.id", "Pattern.java.lang.String", "Pattern" ], "arguments":[ { "codes":[ "userInfo.id", "id" ], "arguments":null, "defaultMessage":"id", "code":"id" }, [], { "defaultMessage":"^[1-9]\\d*$", "arguments":null, "codes":[ "^[1-9]\\d*$" ] } ], "defaultMessage":"主键范围不正确", "objectName":"userInfo", "field":"id", "rejectedValue":"1A2", "bindingFailure":false, "code":"Pattern" }, "requestid":"076c899495b448b59f1b133efd130061" }
控制台输出
可以看到第一次请求时WebLogAspect成功打印了请求数据和返回结果,而第二次因为没有通过校验,没有进入WebLogAspect,所以没有打印数据
2019-11-2122:50:43.283INFO94432---[nio-9004-exec-2]com.zero.check.aspect.WebLogAspect:REQUEST:{ "id":"1", "userList":[ { "userId":"Google", "userName":"http://www.google.com" }, { "userId":"S", "userName":"http://www.SoSo.com" }, { "userId":"SoSo", "userName":"http://www.SoSo.com" } ], "roleId":"11" } 2019-11-2122:50:43.345INFO94432---[nio-9004-exec-2]com.zero.check.aspect.WebLogAspect:RESPONSE:{"code":"ok","data":"Hello1","requestid":"286174a075c144eeb0de0b8dbd7c1851"}
GET接口
localhost:9004/check/userValidGet?userId=a&userName=zero
返回结果
{ "code":"ok", "data":"Hellozero", "requestid":"9b5ea9bf1db64014b0b4d445d8baf9dc" } localhost:9004/check/userValidGet?userId=a&userName=
返回结果
{ "code":"error", "message":"用户名称不能为空", "data":{ "codes":[ "NotBlank.user.userName", "NotBlank.userName", "NotBlank.java.lang.String", "NotBlank" ], "arguments":[ { "codes":[ "user.userName", "userName" ], "arguments":null, "defaultMessage":"userName", "code":"userName" } ], "defaultMessage":"用户名称不能为空", "objectName":"user", "field":"userName", "rejectedValue":"", "bindingFailure":false, "code":"NotBlank" }, "requestid":"5677d93c084d418e88cf5bb8547c5a2e" }
控制台输出
可以看到第一次请求时WebLogAspect成功打印了请求和返回结果,而第二次因为没有通过校验,没有进入WebLogAspect,所以没有打印数据
2019-11-2123:18:50.755INFO94432---[nio-9004-exec-9]com.zero.check.aspect.WebLogAspect:REQUEST:{userName=zero,userId=a} 2019-11-2123:18:50.756INFO94432---[nio-9004-exec-9]com.zero.check.aspect.WebLogAspect:RESPONSE:{"code":"ok","data":"Hellozero","requestid":"422edc9cd59d45bea275e579a67ccd0c"}
5.代码Git地址
git@github.com:A-mantis/SpringBootDataCheck.git
总结
以上所述是小编给大家介绍的Springboot使用@Valid和AOP做参数校验及日志输出问题,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。