Springboot使用@Valid 和AOP做参数校验及日志输出问题

作者:zero 时间:2023-12-05 04:39:12 

项目背景

最近在项目上对接前端的的时候遇到了几个问题

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


package com.zero.check.query;
import lombok.Data;
import org.hibernate.validator.constraints.EAN;
import org.springframework.stereotype.Component;
import javax.validation.Valid;
import javax.validation.constraints.*;
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


package com.zero.check.query;

import lombok.Data;
import org.springframework.stereotype.Component;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.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
@Data
public class User {
 @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
package com.zero.check.controller;
import com.zero.check.query.User;
import com.zero.check.query.UserInfo;
import com.zero.check.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 " + userInfo.getId());
 }
 @GetMapping(value = "/userValidGet")
 public Response queryUserGet(@Valid User user, BindingResult result) {
   return Response.ok().setData("Hello " + user.getUserName());
 }
}

3.AOP

定义切点@Pointcut("execution( com.zero.check.controller.. (..))"),定义后可监控com.zero.check.controller包和子包里任意方法的执行

如果输入参数不能通过校验,就直接抛出异常,由于定义了UserInfoHandler * ,可以拦截处理校验错误,这样就可以省略大量的非空判断,让Controller层专注业务代码,并且将日志集中在WebLogAspect中处理,不会因为记录日志导致要到处改代码


if (bindingResult.hasErrors()) {
     FieldError error = bindingResult.getFieldError();
     throw new UserInfoException(Response.error(error.getDefaultMessage()).setData(error));
   }

UserInfoHandler
package com.zero.check.handler;

import com.zero.check.exception.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) {
   return e.getR();
 }
}

WebLogAspect


package com.zero.check.aspect;

import com.alibaba.fastjson.JSON;
import com.zero.check.exception.UserInfoException;
import com.zero.check.utils.Response;
import org.aspectj.lang.JoinPoint;
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.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.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 = LoggerFactory.getLogger(WebLogAspect.class);
 private final String REQUEST_GET = "GET";
 private final String REQUEST_POST = "POST";
 /**
  * 定义切点,切点为com.zero.check.controller包和子包里任意方法的执行
  */
 @Pointcut("execution(* com.zero.check.controller..*(..))")
 public void webLog() {
 }
 /**
  * 前置通知,在切点之前执行的通知
  *
  * @param joinPoint 切点
  */
 @Before("webLog() &&args(..,bindingResult)")
 public void doBefore(JoinPoint joinPoint, BindingResult bindingResult) {
   if (bindingResult.hasErrors()) {
     FieldError error = bindingResult.getFieldError();
     throw new UserInfoException(Response.error(error.getDefaultMessage()).setData(error));
   }
   //获取请求参数
   try {
     String reqBody = this.getReqBody();
     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: " + JSON.toJSONString(ret));
   } catch (Exception ex) {
     logger.info("get Response Error: " + ex.getMessage());
   }
 }
 /**
  * 返回调用参数
  *
  * @return ReqBody
  */
 private String getReqBody() {
   //从获取RequestAttributes中获取HttpServletRequest的信息
   HttpServletRequest request = this.getHttpServletRequest();
   //获取请求方法GET/POST
   String method = request.getMethod();
   Optional.ofNullable(method).orElse("UNKNOWN");
   if (REQUEST_POST.equals(method)) {
     return this.getPostReqBody(request);
   } else if (REQUEST_GET.equals(method)) {
     return this.getGetReqBody(request);
   }
   return "get Request Parameter Error";
 }
 /**
  * 获取request
  * Spring对一些(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态的bean采用ThreadLocal进行处理
  * 让它们也成为线程安全的状态
  *
  * @return
  */
 private HttpServletRequest getHttpServletRequest() {
   //获取RequestAttributes
   RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
   return (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
 }
 /**
  * 获取GET请求数据
  *
  * @param request
  * @return
  */
 private String getGetReqBody(HttpServletRequest request) {
   Enumeration<String> enumeration = request.getParameterNames();
   Map<String, String> parameterMap = new HashMap<>(16);
   while (enumeration.hasMoreElements()) {
     String parameter = enumeration.nextElement();
     parameterMap.put(parameter, request.getParameter(parameter));
   }
   return parameterMap.toString();
 }
 /**
  * 获取POST请求数据
  *
  * @param request
  * @return 返回POST参数
  */
 private String getPostReqBody(HttpServletRequest request) {
   StringBuilder stringBuilder = new StringBuilder();
   try (InputStream inputStream = request.getInputStream();
      BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
     char[] charBuffer = new char[128];
     int bytesRead = -1;
     while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
       stringBuilder.append(charBuffer, 0, bytesRead);
     }
   } catch (IOException e) {
     logger.info("get Post Request Parameter err : " + e.getMessage());
   }
   return stringBuilder.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": "Hello 12",
 "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-21 22:50:43.283 INFO 94432 --- [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-21 22:50:43.345 INFO 94432 --- [nio-9004-exec-2] com.zero.check.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,
   "code": "NotBlank"
 },
 "requestid": "5677d93c084d418e88cf5bb8547c5a2e"
}

控制台输出

可以看到第一次请求时WebLogAspect成功打印了请求和返回结果,而第二次因为没有通过校验,没有进入WebLogAspect,所以没有打印数据


2019-11-21 23:18:50.755 INFO 94432 --- [nio-9004-exec-9] com.zero.check.aspect.WebLogAspect    : REQUEST: {userName=zero, userId=a}
2019-11-21 23:18:50.756 INFO 94432 --- [nio-9004-exec-9] com.zero.check.aspect.WebLogAspect    : RESPONSE: {"code":"ok","data":"Hello zero","requestid":"422edc9cd59d45bea275e579a67ccd0c"}

5.代码Git地址

git@github.com:A-mantis/SpringBootDataCheck.git

总结

以上所述是小编给大家介绍的Springboot使用@Valid 和AOP做参数校验及日志输出问题网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

来源:https://segmentfault.com/a/1190000021082382

标签:spring,boot,参数,校验,日志输出
0
投稿

猜你喜欢

  • 详解ThreadLocal为什么会内存溢出原理

    2023-11-09 18:45:26
  • android 控件同时监听单击和双击实例

    2022-11-16 15:45:33
  • C# 最基础知识介绍--多态

    2022-07-10 14:34:36
  • Android中Webview打开网页的同时发送HTTP头信息方法

    2022-05-19 20:01:28
  • 详解Mybatis注解写法(附10余个常用例子)

    2023-01-19 03:39:53
  • Android开发之开关按钮用法示例

    2021-12-24 22:16:11
  • 快速解决设置Android 23.0以上版本对SD卡的读写权限无效的问题

    2021-08-26 09:31:43
  • java HashMap通过value反查key的代码示例

    2022-06-07 08:50:55
  • 基于Kubernetes实现前后端应用的金丝雀发布(两种方案)

    2023-01-07 02:32:27
  • Java微信公众平台开发(12) 微信用户信息的获取

    2023-05-26 07:28:56
  • Android如何使用RecyclerView打造首页轮播图

    2022-06-08 13:15:39
  • Java中的静态内部类详解及代码示例

    2021-11-25 08:50:00
  • Service Activity的三种交互方式(详解)

    2022-03-16 20:55:08
  • springboot2.x只需两步快速整合log4j2的方法

    2023-06-06 20:41:21
  • C++选择排序算法实例

    2021-10-27 21:43:33
  • java8 stream的多字段排序实现(踩坑)

    2023-10-22 11:06:31
  • Android实现按钮点击效果

    2021-06-10 10:05:54
  • Mybatisplus主键生成策略算法解析

    2022-06-22 20:49:23
  • Springboot 跨域配置无效及接口访问报错的解决方法

    2021-10-02 01:04:11
  • 详解Android应用中ListView列表选项栏的编写方法

    2023-09-03 15:01:25
  • asp之家 软件编程 m.aspxhome.com