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

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做参数校验及日志输出问题,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

(0)

相关推荐

  • 详解如何在Spring Boot项目使用参数校验

    开发web项目有时候我们需要对controller层传过来的参数进行一些基本的校验,比如非空,非null,整数值的范围,字符串的个数,日期,邮箱等等.最常见的就是我们直接写代码校验,这样以后比较繁琐,而且不够灵活. Bean Validation 1.0(JSR-303)是一个校验规范,在spring Boot项目由于自带了hibernate validator 5(http://hibernate.org/validator/)实现,所以我们可以非常方便的使用这个特性 . 核心的pom依赖:

  • SpringBoot通过yml和xml文件配置日志输出方法

    SpringBoot中默认使用Logback进行日志输出,可以同时使用SpringBoot框架的配置文件application.yml或是通过logback的配置文件logback.xml进行配置. 通过application.yml配置 <?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!--定义日志文件的存储地址 勿在 Lo

  • Spring boot进行参数校验的方法实例详解

    Spring boot开发web项目有时候我们需要对controller层传过来的参数进行一些基本的校验,比如非空.整数值的范围.字符串的长度.日期.邮箱等等.Spring支持JSR-303 Bean Validation API,可以方便的进行校验. 使用注解进行校验 先定义一个form的封装对象 class RequestForm { @Size(min = 1, max = 5) private String name; public String getName() { return n

  • Spring Boot异步输出Logback日志方法详解

    一.介绍 1.1 Logback Logback是由log4j创始人设计的另一个开源日志组件,它分为下面下个模块: logback-core:其它两个模块的基础模块 logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API使你可以很方便地更换成其它日志系统如log4j或JDK14 Logging logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能 1.2 日志级别 包括:TRACE.DEBUG.INFO.WARN

  • Spring Boot实现通用的接口参数校验

    本文介绍基于 Spring Boot 和 JDK8 编写一个 AOP ,结合自定义注解实现通用的接口参数校验. 缘由 目前参数校验常用的方法是在实体类上添加注解,但对于不同的方法,所应用的校验规则也是不一样的,例如有一个 AccountVO 实体: public class AccountVO { private String name; // 姓名 private Integer age; // 年龄 } 假设存在这样一个业务:用户注册时需要填写姓名和年龄,用户登陆时只需要填写姓名就可以了.那

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

    项目背景 最近在项目上对接前端的的时候遇到了几个问题 1.经常要问前端要请求参数 2.要根据请求参数写大量if...else,代码散步在 Controller 中,影响代码质量 3.为了解决问题1,到处记日志,导致到处改代码 解决方案 为了解决这类问题,我使用了@Valid 做参数校验,并使用AOP记录前端请求日志 1.Bean实体类增加注解 对要校验的实体类增加注解,如果实体类中有List结构,就在List上加@Valid @Valid注解 注解 备注 @Null 只能为null @NotNu

  • SpringBoot使用validation做参数校验的实现步骤

    1.添加依赖 直接添加 hibernate-validator <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.2.Final</version> </dependency> 添加spring-boot-starter-validat

  • SpringBoot使用validation做参数校验说明

    目录 1.添加依赖 直接添加 hibernate-validator 添加spring-boot-starter-validation 添加spring-boot-starter-web 2. 配置文件 3.统一异常处理 4.使用 1.添加依赖 直接添加 hibernate-validator <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-valid

  • Spring/Spring Boot 中优雅地做参数校验拒绝 if/else 参数校验

    数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据. 最普通的做法就像下面这样.我们通过 if/else 语句对请求的每一个参数一一校验. @RestController @RequestMapping("/api/person") public class PersonController { @PostMapping public ResponseEnti

  • 使用@Valid+BindingResult进行controller参数校验方式

    目录 @Valid+BindingResult进行controller参数校验 Controller层方法的参数校验 全局统一异常拦截器 @Valid+BindingResult进行controller参数校验 由于controller是调用的第一层,经常参数校验将在这里完成,常见有非空校验.类型校验等,常见写法为以下伪代码: public void round(Object a){ if(a.getLogin() == null){ return "手机号不能为空!"; } } 但是

  • 详解SpringBoot中的参数校验(项目实战)

    Java后端发工作中经常会对前端传递过来的参数做一些校验,在业务中还要抛出异常或者不断的返回异常时的校验信息,充满了if-else这种校验代码,在代码中相当冗长.例如说,用户注册时,会校验手机格式的正确性,用户名的长度等等.虽说前端也可以做参数校验,但是为了保证我们API接口的可靠性,以保证最终数据入库的正确性,后端进行参数校验不可忽视. Hibernate Validator 提供了一种统一方便的方式,让我们快速的实现参数校验. Hibernate Validator 使用注解,实现声明式校验

  • SpringBoot实现接口的各种参数校验的示例

    目录 1.添加依赖 2.接口参数校验 2.1 requestBody参数校验 2.2 requestParam/PathVariable参数校验 3.统一异常处理 4.进阶使用 4.1 分组校验 4.2 嵌套校验 4.3 集合校验 4.4 自定义校验 5.快速失败 (Fail Fast) 6.@Valid和@Validated区别 7.实现原理 7.1 requestBody参数校验实现原理 7.2 方法级别的参数校验实现原理 在我们进行接口开发时,在对参数的接收时,我们需要冗余复杂的校验规则

  • SpringBoot各种参数校验的实例教程

    目录 简单使用 引入依赖 requestBody参数校验 requestParam/PathVariable参数校验 统一异常处理 进阶使用 分组校验 嵌套校验 集合校验 自定义校验 编程式校验 快速失败(Fail Fast) @Valid和@Validated区别 实现原理 requestBody参数校验实现原理 方法级别的参数校验实现原理 总结 简单使用 Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现.hibernate valida

  • java开发SpringBoot参数校验过程示例教程

    目录 为什么需要参数校验 SpringBoot中集成参数校验 第一步,引入依赖 第二步,定义要参数校验的实体类 常见的约束注解如下: 第三步,定义校验类进行测试 第四步,体验效果 自定义参数校验 第一步,创建自定义注解 第二步,自定义校验逻辑 第三步,在字段上增加注解 第四步,体验效果 分组校验 第一步:定义分组接口 第二步,在模型中给参数分配分组 第三步,给需要参数校验的方法指定分组 第四步,体验效果 小结 大家好,我是飘渺. 前几天写了一篇SpringBoot如何统一后端返回格式?老鸟们都是

  • SpringBoot如何进行参数校验实例详解

    目录 前言 为什么需要参数校验 SpringBoot中集成参数校验 第一步,引入依赖 第二步,定义要参数校验的实体类 第三步,定义校验类进行测试 第四步,体验效果 参数异常加入全局异常处理器 体验效果 自定义参数校验 第一步,创建自定义注解 第二步,自定义校验逻辑 第三步,在字段上增加注解 第四步,体验效果 分组校验 第一步:定义分组接口 第二步,在模型中给参数分配分组 第三步,给需要参数校验的方法指定分组 第四步,体验效果 小结 总结 前言 今天我们继续聊聊在SprinBoot中如何集成参数校

随机推荐