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必须为Email,也可以通过正则表达式和flag指定⾃定义的email格式
UserInfo
heck.query;
import lombok.Data;
import org.straints.EAN;
import org.springframework.stereotype.Component;
import javax.validation.Valid;
import straints.*;
import java.util.List;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 15:05
* @history: 1.2019/11/21 created by wei.wang
*/
@Component
@Data
public class UserInfo {
@NotBlank(message = "主键不能为空")
@Pattern(regexp = "^[1-9]\\d*$",message = "主键范围不正确")
private String id;
@Valid
@NotEmpty(message = "⽤户列表不能为空")
private List<User> userList;
@NotNull(message = "权限不能为空")
@Min(value = 1, message = "权限范围为[1-99]")
@Max(value = 99, message = "权限范围为[1-99]")
private Long roleId;
}
User
heck.query;
import lombok.Data;
import org.springframework.stereotype.Component;
import straints.NotBlank;
import straints.NotNull;
import java.util.List;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 16:03
* @history: 1.2019/11/21 created by wei.wang
*/
@Component
@NotBlank(message = "⽤户⼯号不能为空")
private String userId;
@NotBlank(message = "⽤户名称不能为空")
private String userName;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
2.Controller层
在需要校验的pojo前边添加@Validated,在需要校验的pojo后边添加BindingResult br接收校验出错信息,需要注意的是, BindingResult result⼀定要跟在
@Validated 注解对象的后⾯(必须是实体类),⽽且当有多个@Validated注解时,每个注解对象后⾯都需要添加⼀个 BindingResult,⽽实际使⽤时由于在WebLogAspect切点读取了请求数据,会导致在Controller层请求参数中读不到数据,这⾥需要修改其他内容,详见Git
DataCheckController
ller;
heck.query.User;
heck.query.UserInfo;
heck.utils.Response;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 14:57
* @history: 1.2019/11/21 created by wei.wang
*/
@RestController
@RequestMapping(value = "/check")
public class DataCheckController {
@PostMapping(value = "/userValidPost")
public Response queryUserPost(@Valid @RequestBody UserInfo userInfo, BindingResult result) {
return Response.ok().setData("Hello " + Id());
}
@GetMapping(value = "/userValidGet")
public Response queryUserGet(@Valid User user, BindingResult result) {
return Response.ok().setData("Hello " + UserName());
}
}
3.AOP
定义切点@Pointcut("execution( ller.. (..))"),定义后可监控ller包和⼦包⾥任意⽅法的执⾏
如果输⼊参数不能通过校验,就直接抛出异常,由于定义了UserInfoHandler,可以拦截处理校验错误,这样就可以省略⼤量的⾮空判断,让Controller层专注业务代码,并且将⽇志集中在WebLogAspect中处理,不会因为记录⽇志导致要到处改代码
if (bindingResult.hasErrors()) {
FieldError error = FieldError();
throw new (DefaultMessage()).setData(error));
}
UserInfoHandler
heck.handler;
ption.UserInfoException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 15:04
* @history: 1.2019/11/21 created by wei.wang
*/
@RestControllerAdvice
public class UserInfoHandler {
/**
* 校验错误拦截处理
*
* @param e 错误信息集合
* @return 错误信息
*/
@ExceptionHandler(UserInfoException.class)
public Object handle(UserInfoException e) {
R();
}
}
WebLogAspect
heck.aspect;
import com.alibaba.fastjson.JSON;
ption.UserInfoException;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.t.request.RequestAttributes;
import org.t.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* @Description:
* @author: wei.wang
* @since: 2019/11/21 13:47
* @history: 1.2019/11/21 created by wei.wang
*/
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger(WebLogAspect.class);
private final String REQUEST_GET = "GET";
private final String REQUEST_POST = "POST";
/**
* 定义切点,切点为ller包和⼦包⾥任意⽅法的执⾏
*/
@Pointcut("execution(* ller..*(..))")
public void webLog() {
}
/**
* 前置通知,在切点之前执⾏的通知
*
* @param joinPoint 切点
*/
@Before("webLog() &&args(..,bindingResult)")
public void doBefore(JoinPoint joinPoint, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
FieldError error = FieldError();
throw new (DefaultMessage()).setData(error));
}
//获取请求参数
try {
String reqBody = ReqBody();
logger.info("REQUEST: " + reqBody);
} catch (Exception ex) {
logger.info("get Request Error: " + ex.getMessage());
}
}
/
**
* 后置通知,切点后执⾏
*
* @param ret
*/
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) {
//处理完请求,返回内容
try {
logger.info("RESPONSE: " + JSONString(ret));
} catch (Exception ex) {
logger.info("get Response Error: " + ex.getMessage());
}
}
/**
* 返回调⽤参数
*
* @return ReqBody
*/
private String getReqBody() {
//从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = HttpServletRequest();
/
/获取请求⽅法GET/POST
String method = Method();
Optional.ofNullable(method).orElse("UNKNOWN");
if (REQUEST_POST.equals(method)) {
PostReqBody(request);
} else if (REQUEST_GET.equals(method)) {
GetReqBody(request);
}
return "get Request Parameter Error";
}
/**
* 获取request
* Spring对⼀些(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中⾮线程安全状态的bean采⽤ThreadLocal进⾏处理 * 让它们也成为线程安全的状态
*
* @return
*/
private HttpServletRequest getHttpServletRequest() {
//获取RequestAttributes
RequestAttributes requestAttributes = RequestAttributes();
return (HttpServletRequest) solveReference(RequestAttributes.REFERENCE_REQUEST);
}
/**
*
* @param request
* @return
*/
private String getGetReqBody(HttpServletRequest request) {
Enumeration<String> enumeration = ParameterNames();
Map<String, String> parameterMap = new HashMap<>(16);
while (enumeration.hasMoreElements()) {
String parameter = Element();
parameterMap.put(parameter, Parameter(parameter));
}
String();
}
/**
* 获取POST请求数据
*
* @param request
* @return 返回POST参数
*/
private String getPostReqBody(HttpServletRequest request) {
StringBuilder stringBuilder = new StringBuilder();springboot aop
try (InputStream inputStream = InputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = ad(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} catch (IOException e) {
logger.info("get Post Request Parameter err : " + e.getMessage());
}
String();
}
}
4.测试
POST接⼝
localhost:9004/check/userValidPost
请求参数
{
"id":"12",
"userList": [
{
"userId": "Google",
"userName": "le"
},
{
"userId": "S",
"userName": "www.SoSo"
},
{
"userId": "SoSo",
"userName": "www.SoSo"
}
],
"roleId":"11"
}
返回结果
{
"code": "ok",
"data": "Hello 12",
"requestid": "706cd81db49d4c9795e5457cebb1ba8c"
}
请求参数
{
"id":"1A2",
"userList": [
{
"userId": "Google",
"userName": "le"
},
{
"userId": "S",
"userName": "www.SoSo"
},
{
"userId": "SoSo",
"userName": "www.SoSo"
}
],
"roleId":"11"
}
返回结果
{
"code": "error",
"message": "主键范围不正确",
"data": {
"codes": [
"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-21 22:50:43.283 INFO 94432 --- [nio-9004-exec-2] heck.aspect.WebLogAspect : REQUEST: {
"id":"1",
"userList": [
{
"userId": "Google",
"userName": "le"
},
{
"userId": "S",
"userName": "www.SoSo"
},
{
"userId": "SoSo",
"userName": "www.SoSo"
}
],
"roleId":"11"
}
2019-11-21 22:50:43.345 INFO 94432 --- [nio-9004-exec-2] heck.aspect.WebLogAspect : RESPONSE: {"code":"ok","data":"Hello 1","requestid":"286174a075c144eeb0de0b8dbd7c1851"} GET接⼝
localhost:9004/check/userValidGet?userId=a&userName=zero
返回结果
{
"code": "ok",
"data": "Hello zero",
"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,
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论