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();
MapparameterMap=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(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。