Springboot使用Valid-和AOP做参数校验以及日志输出

35次阅读

共计 10207 个字符,预计需要花费 26 分钟才能阅读完成。

项目背景

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

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

正文完
 0